detached 72f621f

feat: Enhance configuration defaults and logging for ACME and proxy

- Added default values for HTTP logs, admin path, and network in configuration.
- Updated applyDefaultsToConfig to utilize new default constants for improved clarity.
- Enhanced logging for applied default values and configuration loading.
- Improved error handling and logging in ACME certificate management and proxy operations.
- Refactored database queries to ensure consistent logging and error handling.
commit
72f621f7f39529368fcb2c695cb7802b304c9b23
author
bamen · · 2025-03-29 11:13 UTC
parent
106dfe3
signature
unsigned
6 files · +325 -220
M internal/common/init_config.go +226 -197
@@ -107,10 +107,13 @@ var (
107107 detectUpstreamProxy = false // Default to disabled
108108 skipCertificates = false // Default to disabled
109109 enableRateLimit = false // Default to disabled
110+ enableHttpLogs = true // Default to enabled
110111 defaultChallengeType = "http-01"
111112 defaultHttpChallengePort = "80"
112113 defaultDnsChallengePropagationTimeout = 60 // seconds
113114 defaultDnsChallengePollingInterval = 5 // seconds
115+ defaultAdminPath = "/admin"
116+ defaultNetwork = "gordon"
114117 )
115118
116119 // applyDefaultsToConfig applies default values to any fields that have zero values
@@ -142,8 +145,8 @@ func applyDefaultsToConfig(config *Config) bool {
142145
143146 // Apply defaults to Admin config
144147 if config.Admin.Path == "" {
145- config.Admin.Path = "/admin"
146- logger.Debug("Applied default value for Admin.Path", "value", "/admin")
148+ config.Admin.Path = defaultAdminPath
149+ logger.Debug("Applied default value for Admin.Path", "value", defaultAdminPath)
147150 defaultsApplied = true
148151 }
149152
@@ -159,8 +162,8 @@ func applyDefaultsToConfig(config *Config) bool {
159162 defaultsApplied = true
160163 }
161164 if config.ContainerEngine.Network == "" {
162- config.ContainerEngine.Network = "gordon"
163- logger.Debug("Applied default value for ContainerEngine.Network", "value", "gordon")
165+ config.ContainerEngine.Network = defaultNetwork
166+ logger.Debug("Applied default value for ContainerEngine.Network", "value", defaultNetwork)
164167 defaultsApplied = true
165168 }
166169
@@ -208,8 +211,8 @@ func applyDefaultsToConfig(config *Config) bool {
208211 if err == nil {
209212 // Only set default if enableHttpLogs not explicitly specified
210213 if !strings.Contains(string(yamlContent), "enableHttpLogs:") {
211- config.ReverseProxy.EnableHttpLogs = true
212- logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", true)
214+ config.ReverseProxy.EnableHttpLogs = enableHttpLogs
215+ logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", enableHttpLogs)
213216 defaultsApplied = true
214217 } else {
215218 logger.Debug("Keeping explicit value for ReverseProxy.EnableHttpLogs", "value", config.ReverseProxy.EnableHttpLogs)
@@ -217,21 +220,21 @@ func applyDefaultsToConfig(config *Config) bool {
217220 } else {
218221 // If we can't read the file, apply the default only if not set
219222 if !config.ReverseProxy.EnableHttpLogs {
220- config.ReverseProxy.EnableHttpLogs = true
221- logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", true)
223+ config.ReverseProxy.EnableHttpLogs = enableHttpLogs
224+ logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", enableHttpLogs)
222225 defaultsApplied = true
223226 }
224227 }
225228 } else {
226229 // If config file doesn't exist yet, set the default
227- config.ReverseProxy.EnableHttpLogs = true
228- logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", true)
230+ config.ReverseProxy.EnableHttpLogs = enableHttpLogs
231+ logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", enableHttpLogs)
229232 defaultsApplied = true
230233 }
231234 // Set EnableRateLimit to false by default if not specified
232235 if !config.ReverseProxy.EnableRateLimit {
233- config.ReverseProxy.EnableRateLimit = false
234- logger.Debug("Applied default value for ReverseProxy.EnableRateLimit", "value", false)
236+ config.ReverseProxy.EnableRateLimit = enableRateLimit
237+ logger.Debug("Applied default value for ReverseProxy.EnableRateLimit", "value", enableRateLimit)
235238 defaultsApplied = true
236239 }
237240
@@ -249,6 +252,28 @@ func applyDefaultsToConfig(config *Config) bool {
249252 defaultsApplied = true
250253 }
251254
255+ // Apply defaults for Let's Encrypt challenge settings
256+ if config.ReverseProxy.DefaultChallengeType == "" {
257+ config.ReverseProxy.DefaultChallengeType = defaultChallengeType
258+ logger.Debug("Applied default value for ReverseProxy.DefaultChallengeType", "value", defaultChallengeType)
259+ defaultsApplied = true
260+ }
261+ if config.ReverseProxy.DefaultHttpChallengePort == "" {
262+ config.ReverseProxy.DefaultHttpChallengePort = defaultHttpChallengePort
263+ logger.Debug("Applied default value for ReverseProxy.DefaultHttpChallengePort", "value", defaultHttpChallengePort)
264+ defaultsApplied = true
265+ }
266+ if config.ReverseProxy.DefaultDnsChallengePropagationTimeout == 0 {
267+ config.ReverseProxy.DefaultDnsChallengePropagationTimeout = defaultDnsChallengePropagationTimeout
268+ logger.Debug("Applied default value for ReverseProxy.DefaultDnsChallengePropagationTimeout", "value", defaultDnsChallengePropagationTimeout)
269+ defaultsApplied = true
270+ }
271+ if config.ReverseProxy.DefaultDnsChallengePollingInterval == 0 {
272+ config.ReverseProxy.DefaultDnsChallengePollingInterval = defaultDnsChallengePollingInterval
273+ logger.Debug("Applied default value for ReverseProxy.DefaultDnsChallengePollingInterval", "value", defaultDnsChallengePollingInterval)
274+ defaultsApplied = true
275+ }
276+
252277 return defaultsApplied
253278 }
254279
@@ -342,30 +367,6 @@ func getConfigDir() (string, error) {
342367 var configDir string
343368
344369 logger.Debug("getConfigDir: Checking if running in WSL")
345- if isWSL() {
346- logger.Debug("getConfigDir: Running in WSL")
347- // Use XDG_CONFIG_HOME for WSL
348- if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
349- logger.Debug("getConfigDir: Using XDG_CONFIG_HOME environment variable", "value", xdgConfigHome)
350- configDir = filepath.Join(xdgConfigHome, "Gordon")
351- } else {
352- // If XDG_CONFIG_HOME is not set, fall back to default locations
353- logger.Debug("getConfigDir: XDG_CONFIG_HOME not set, getting user home directory")
354- homeDir, err := os.UserHomeDir()
355- if err != nil {
356- // If we can't get the home directory, use a fallback
357- logger.Debug("getConfigDir: Failed to get user home directory, using temp dir", "error", err)
358- homeDir = os.TempDir() // Use the system's temp directory as a fallback
359- logger.Warn("Warning: Unable to determine home directory. Using temp directory", "dir", homeDir)
360- }
361-
362- configDir = filepath.Join(homeDir, ".config", "Gordon")
363- logger.Debug("getConfigDir: Set config directory based on home dir", "dir", configDir)
364- }
365-
366- logger.Debug("getConfigDir: Returning WSL config directory", "dir", configDir)
367- return configDir, nil
368- }
369370
370371 logger.Debug("getConfigDir: Checking if running in container")
371372 if docker.IsRunningInContainer() {
@@ -418,75 +419,106 @@ func (config *Config) LoadConfig() (*Config, error) {
418419 // If we're in a non-interactive environment, use simplified config
419420 nonInteractiveEnv := os.Getenv("GORDON_NONINTERACTIVE")
420421 isNonInteractive := nonInteractiveEnv == "true" || nonInteractiveEnv == "1" || nonInteractiveEnv == "yes"
422+ isInContainer := docker.IsRunningInContainer() // Check once
423+
424+ // Flag to track if saving is needed at the end
425+ shouldSaveConfig := false
426+ defaultsApplied := false
421427
422428 if isNonInteractive {
423429 logger.Info("Non-interactive mode detected, using simplified config without prompts")
424- // Create base configuration without prompts
425- homeDir, err := os.UserHomeDir()
426- if err != nil {
427- homeDir = "/tmp"
428- logger.Warn("Could not get user home directory, using /tmp instead")
429- }
430+ // Start with an empty config
431+ *config = Config{}
430432
431- // Get default socket paths
432- sock := "/var/run/docker.sock"
433- podmansock := "/run/podman/podman.sock"
433+ // Determine StorageDir
434+ if isInContainer {
435+ config.General.StorageDir = "/data"
436+ logger.Debug("Non-interactive: Using default container storage dir", "dir", config.General.StorageDir)
437+ } else {
438+ homeDir, err := os.UserHomeDir()
439+ if err != nil {
440+ homeDir = "/tmp"
441+ logger.Warn("Could not get user home directory, using /tmp instead")
442+ }
443+ config.General.StorageDir = filepath.Join(homeDir, ".gordon")
444+ logger.Debug("Non-interactive: Using default host storage dir", "dir", config.General.StorageDir)
445+ }
434446
435- // Auto-detect Podman
447+ // Auto-detect Podman and set relevant fields
436448 isPodman, podmanSocket := docker.DetectPodman()
449+ config.ContainerEngine.Podman = isPodman
437450 if isPodman {
438- podmansock = podmanSocket
451+ config.ContainerEngine.PodmanSock = podmanSocket
452+ logger.Info("Non-interactive: Podman detected", "socket", podmanSocket)
453+ } else {
454+ // Explicitly set Docker sock if Podman not detected, applyDefaults might override if empty later
455+ config.ContainerEngine.Sock = sock
456+ logger.Info("Non-interactive: Assuming Docker")
439457 }
440458
441- *config = Config{
442- General: GeneralConfig{
443- StorageDir: homeDir + "/.gordon",
444- LogLevel: defaultLogLevel,
445- },
446- Http: HttpConfig{
447- Port: "8080",
448- Domain: "localhost",
449- Https: false,
450- },
451- Admin: AdminConfig{
452- Path: "/admin",
453- },
454- ContainerEngine: ContainerEngineConfig{
455- Sock: sock,
456- PodmanSock: podmansock,
457- Podman: isPodman, // Use the auto-detected value
458- Network: "gordon",
459- },
460- ReverseProxy: ReverseProxyConfig{
461- Port: reverseProxyPort,
462- HttpPort: httpPort,
463- CertDir: certDir,
464- RenewBefore: renewBefore,
465- LetsEncryptMode: letsEncryptMode,
466- GracePeriod: gracePeriod,
467- EnableHttpLogs: true,
468- EnableRateLimit: false,
469- },
459+ // Apply remaining defaults
460+ defaultsApplied = applyDefaultsToConfig(config)
461+ if defaultsApplied {
462+ logger.Info("Non-interactive: Applied default configuration values")
463+ shouldSaveConfig = true // Mark for saving if defaults were needed
470464 }
471465
472- // If in container, apply env vars
473- if docker.IsRunningInContainer() {
474- logger.Info("Running in container, overriding configuration with environment variables...")
466+ // If in container, apply env vars (potentially overriding defaults)
467+ if isInContainer {
468+ logger.Info("Non-interactive: Running in container, overriding configuration with environment variables...")
469+ // Assuming loadConfigFromEnv modifies config in place
470+ loadConfigFromEnv(config, !configLogsPrinted) // Pass flag to control initial logging
471+ configLogsPrinted = true // Mark logs as printed
472+ shouldSaveConfig = true // Mark for saving if env vars were applied
473+ logger.Debug("Non-interactive: Finished loading config from environment")
474+ }
475475
476- loadConfigFromEnv(config, true)
476+ // Load run env (common step)
477+ config.Build.RunEnv = os.Getenv("RUN_ENV")
478+ if config.Build.RunEnv == "" {
479+ config.Build.RunEnv = "prod"
480+ logger.Info("Non-interactive: No RUN_ENV specified, defaulting to 'prod'")
481+ } else {
482+ logger.Info("Non-interactive: Run environment set", "env", config.Build.RunEnv)
477483 }
478484
479- return config, nil
485+ // Check AdminWebUI (common step)
486+ if config.Admin.Path == "" {
487+ logger.Warn("Non-interactive: Admin path is empty or not set, admin dashboard webui will be disabled")
488+ config.Admin.AdminWebUI = false
489+ } else {
490+ config.Admin.AdminWebUI = true
491+ }
492+
493+ // Apply log level (common step)
494+ if config.General.LogLevel != "" {
495+ logger.GetLogger().SetLogLevel(config.General.LogLevel)
496+ logger.Debug("Non-interactive: Log level set from configuration", "level", config.General.LogLevel)
497+ }
498+
499+ // Save config if needed (only in non-interactive mode if defaults/env applied)
500+ if shouldSaveConfig {
501+ logger.Info("Non-interactive: Saving configuration...")
502+ err := config.SaveConfig()
503+ if err != nil {
504+ // Log error but continue, as config is loaded, just not saved
505+ logger.Error("Non-interactive: Failed to save configuration", "error", err)
506+ } else {
507+ logger.Info("Non-interactive: Successfully saved configuration")
508+ }
509+ }
510+
511+ logger.Debug("LoadConfig: Completed non-interactive setup")
512+ return config, nil // Return early for non-interactive mode
480513 }
481514
482- // Original LoadConfig implementation continues below for interactive mode
483- logger.Debug("LoadConfig: Starting LoadConfig function")
515+ // ---- Interactive Mode Logic ----
516+ logger.Debug("LoadConfig: Starting interactive LoadConfig function")
484517
485518 // Log current execution environment for debugging
486- isInContainer := docker.IsRunningInContainer()
487519 cwd, _ := os.Getwd()
488520 logger.Debug("LoadConfig: Environment information",
489- "isContainer", isInContainer,
521+ "isContainer", isInContainer, // Use cached value
490522 "currentDir", cwd,
491523 "ENV", os.Getenv("ENV"),
492524 "GORDON_NONINTERACTIVE", os.Getenv("GORDON_NONINTERACTIVE"))
@@ -510,11 +542,15 @@ func (config *Config) LoadConfig() (*Config, error) {
510542 logger.Debug("LoadConfig: Got config directory", "dir", configDir)
511543 logger.Info("Using configuration directory", "dir", configDir)
512544
513- // Check for directly mounted config.yml in root when in container
545+ // Determine config file path
514546 var configFilePath string
515- logger.Debug("LoadConfig: Checking if running in container")
516- if docker.IsRunningInContainer() && fileExists("./config.yml") {
517- configFilePath = "./config.yml"
547+ logger.Debug("LoadConfig: Checking config file location")
548+ // Check for direct mount first only if in container
549+ directMountPath := "./config.yml"
550+ useDirectMount := isInContainer && fileExists(directMountPath)
551+
552+ if useDirectMount {
553+ configFilePath = directMountPath
518554 logger.Info("Found directly mounted config.yml in container current directory")
519555 } else {
520556 configFilePath = filepath.Join(configDir, "config.yml")
@@ -526,43 +562,50 @@ func (config *Config) LoadConfig() (*Config, error) {
526562 logger.Info("Reloading configuration (already loaded previously)...")
527563 }
528564
529- configExists := true
530- var defaultsApplied bool = false
565+ configExists := true // Assume exists unless stat fails
531566
532567 logger.Debug("LoadConfig: Checking if config file exists", "path", configFilePath)
533568 _, err = os.Stat(configFilePath)
534569 if errors.Is(err, fs.ErrNotExist) {
535570 configExists = false
571+ shouldSaveConfig = true // Need to save if we create it
536572 logger.Info("Config file not found, creating it", "path", configFilePath)
537573
538- // Create config dir if it doesn't exist
539- logger.Debug("LoadConfig: Creating config directory", "dir", configDir)
540- err = os.MkdirAll(configDir, 0755)
541- if err != nil {
542- logger.Error("LoadConfig: Failed to create config directory", "error", err)
543- return nil, fmt.Errorf("error creating configuration directory: %w", err)
574+ // Create config dir if it doesn't exist (only needed if using configDir path)
575+ if !useDirectMount {
576+ logger.Debug("LoadConfig: Creating config directory", "dir", configDir)
577+ err = os.MkdirAll(configDir, 0755)
578+ if err != nil {
579+ logger.Error("LoadConfig: Failed to create config directory", "error", err)
580+ return nil, fmt.Errorf("error creating configuration directory: %w", err)
581+ }
582+ } else {
583+ logger.Debug("LoadConfig: Using direct mount, skipping config directory creation")
544584 }
545585
546- // Create config file with the default values of ContainerEngineConfig
547- logger.Debug("LoadConfig: Getting user home directory")
548- homeDir, err := os.UserHomeDir()
549- if err != nil {
550- logger.Error("LoadConfig: Failed to get user home directory", "error", err)
551- return nil, fmt.Errorf("could not get user home directory: %w", err)
552- }
586+ // Start with an empty config
587+ *config = Config{}
553588
554- // Check if running in container before creating the config
555- logger.Debug("LoadConfig: Checking if running in container for podman setting")
556- isInContainer := docker.IsRunningInContainer()
557- logger.Info("Is running in container", "value", isInContainer)
589+ // Set specific initial values before applying defaults
558590
559- // Set default podman value based on environment
560- defaultPodman := false
561- // Check for non-interactive mode flag
562- nonInteractive := os.Getenv("GORDON_NONINTERACTIVE") == "true"
591+ // Storage Directory
592+ if isInContainer {
593+ config.General.StorageDir = "/data"
594+ logger.Debug("LoadConfig: Setting initial StorageDir for container", "value", config.General.StorageDir)
595+ } else {
596+ homeDir, err := os.UserHomeDir()
597+ if err != nil {
598+ logger.Error("LoadConfig: Failed to get user home directory", "error", err)
599+ // Setting a temporary default, applyDefaultsToConfig will handle it properly later if needed
600+ homeDir = "/tmp"
601+ }
602+ config.General.StorageDir = filepath.Join(homeDir, ".gordon")
603+ logger.Debug("LoadConfig: Setting initial StorageDir for host", "value", config.General.StorageDir)
604+ }
563605
564- // Only prompt if not in a container and not in non-interactive mode
565- if !isInContainer && !nonInteractive {
606+ // Podman Setting
607+ var defaultPodman bool
608+ if !isInContainer { // Only prompt if interactive and not in a container
566609 logger.Info("Not in container, prompting for podman preference...")
567610 logger.Debug("LoadConfig: About to prompt for podman preference")
568611 defaultPodman = ReadUserInputNonBlocking("Are you using podman ? (y/n)", "n") == "y"
@@ -571,43 +614,18 @@ func (config *Config) LoadConfig() (*Config, error) {
571614 defaultPodman = true
572615 logger.Info("Podman container engine detected via environment variable")
573616 } else {
617+ // Default to false (Docker) in container if env var not set
618+ defaultPodman = false
574619 logger.Info("Using Docker as the default container engine in container environment")
575620 }
621+ config.ContainerEngine.Podman = defaultPodman
622+ logger.Debug("LoadConfig: Set initial podman value", "value", defaultPodman)
576623
577- logger.Debug("LoadConfig: Creating default config structure")
578- *config = Config{
579- General: GeneralConfig{
580- StorageDir: homeDir + "/.gordon",
581- LogLevel: defaultLogLevel,
582- },
583- Http: HttpConfig{
584- Port: "8080",
585- Domain: "localhost",
586- Https: true,
587- },
588- Admin: AdminConfig{
589- Path: "/admin",
590- },
591- ContainerEngine: ContainerEngineConfig{
592- Sock: sock,
593- PodmanSock: podmansock,
594- Podman: defaultPodman,
595- Network: "gordon",
596- },
597- ReverseProxy: ReverseProxyConfig{
598- Port: reverseProxyPort,
599- HttpPort: httpPort,
600- CertDir: certDir,
601- RenewBefore: renewBefore,
602- LetsEncryptMode: letsEncryptMode,
603- GracePeriod: gracePeriod,
604- EnableHttpLogs: true,
605- EnableRateLimit: false,
606- },
607- }
624+ // Now apply all other defaults
625+ logger.Info("Applying default values for the new configuration...")
626+ defaultsApplied = applyDefaultsToConfig(config)
627+ // defaultsApplied is already true implicitly because we created the file
608628
609- // In this case, all defaults were applied since we created a new config
610- defaultsApplied = true
611629 } else if err != nil {
612630 logger.Error("LoadConfig: Error checking if config file exists", "error", err)
613631 return nil, fmt.Errorf("error checking configuration file: %w", err)
@@ -615,7 +633,17 @@ func (config *Config) LoadConfig() (*Config, error) {
615633 // If config file exists, read it
616634 logger.Info("Reading existing configuration file...")
617635 logger.Debug("LoadConfig: About to read and unmarshal existing config file")
618- err = readAndUnmarshalConfig(os.DirFS(filepath.Dir(configFilePath)), filepath.Base(configFilePath), config)
636+ // Use DirFS based on whether it's a direct mount or in configDir
637+ var baseDir string
638+ var fileName string
639+ if useDirectMount {
640+ baseDir = "."
641+ fileName = "config.yml"
642+ } else {
643+ baseDir = configDir
644+ fileName = "config.yml"
645+ }
646+ err = readAndUnmarshalConfig(os.DirFS(baseDir), fileName, config)
619647 if err != nil {
620648 logger.Error("LoadConfig: Failed to read/unmarshal config file", "error", err)
621649 return nil, err
@@ -625,58 +653,37 @@ func (config *Config) LoadConfig() (*Config, error) {
625653 // Apply default values to fields that were not specified in the config file
626654 logger.Info("Applying default values to missing configuration fields...")
627655 defaultsApplied = applyDefaultsToConfig(config)
656+ if defaultsApplied {
657+ shouldSaveConfig = true // Mark for saving if defaults were applied to existing file
658+ }
628659 }
629660
630- // Only load environment variables once, after either creating a new config or reading an existing one
631- logger.Debug("LoadConfig: Checking if running in container for env vars")
632- if docker.IsRunningInContainer() {
633- // Only print logs if we haven't printed them before
661+ // --- Common Steps for Interactive Mode (after file read/creation) ---
662+
663+ // Load environment variables if in a container (might override file/defaults)
664+ envVarsLoaded := false
665+ if isInContainer {
634666 shouldPrintLogs := !configLogsPrinted
635667 logger.Info("Running in container, overriding configuration with environment variables...")
636668 logger.Debug("LoadConfig: About to load config from environment")
637- loadConfigFromEnv(config, shouldPrintLogs)
669+ loadConfigFromEnv(config, shouldPrintLogs) // Assuming modification in place
670+ envVarsLoaded = true
638671 configLogsPrinted = true
672+ shouldSaveConfig = true // Mark for saving if env vars were applied
639673 logger.Debug("LoadConfig: Finished loading config from environment")
640674 }
641675
642676 // Load run env
643677 logger.Debug("LoadConfig: Setting run environment")
644678 config.Build.RunEnv = os.Getenv("RUN_ENV")
645- logger.Info("Run environment", "env", config.Build.RunEnv)
646-
647679 if config.Build.RunEnv == "" {
648680 config.Build.RunEnv = "prod"
649681 logger.Info("No RUN_ENV specified, defaulting to 'prod'")
650- }
651-
652- // If we created a new config or applied defaults to an existing one, save it
653- if !configExists || defaultsApplied {
654- logger.Info("Saving configuration with default values...")
655- logger.Debug("LoadConfig: About to save configuration with defaults")
656- err = config.SaveConfig()
657- if err != nil {
658- logger.Error("LoadConfig: Failed to save configuration", "error", err)
659- return nil, fmt.Errorf("error saving configuration: %w", err)
660- }
661- logger.Debug("LoadConfig: Successfully saved configuration with defaults")
662- }
663-
664- // Debug output to verify config was loaded correctly
665- // Only print this debug information if we're also printing the env var logs
666- if !configLogsPrinted {
667- logger.Info("Loaded configuration from", "path", configFilePath)
668- logger.Info("Container engine config",
669- "docker_socket", config.ContainerEngine.Sock,
670- "podman_socket", config.ContainerEngine.PodmanSock,
671- "using_podman", config.ContainerEngine.Podman,
672- "network", config.ContainerEngine.Network)
673682 } else {
674- // Just print a simplified message
675- logger.Info("Loaded configuration from", "path", configFilePath)
683+ logger.Info("Run environment set", "env", config.Build.RunEnv)
676684 }
677685
678- // After loading from file or environment, check if Admin.Path is empty
679- // This should be placed before returning the config
686+ // Check AdminWebUI status
680687 if config.Admin.Path == "" {
681688 logger.Warn("Admin path is empty or not set, admin dashboard webui will be disabled")
682689 config.Admin.AdminWebUI = false
@@ -684,26 +691,29 @@ func (config *Config) LoadConfig() (*Config, error) {
684691 config.Admin.AdminWebUI = true
685692 }
686693
687- // Save config file if running in container and env vars were applied
688- // This ensures config.yml always reflects the environment variables
689- if docker.IsRunningInContainer() && configLogsPrinted {
690- logger.Info("Environment variables were applied, saving configuration to reflect current values...")
691- err = config.SaveConfig()
694+ // Save config file if it was created, defaults were applied, or env vars loaded in container
695+ if shouldSaveConfig {
696+ logger.Info("Saving final configuration state...", "path", configFilePath)
697+ logger.Debug("LoadConfig: About to save configuration", "reason", fmt.Sprintf("configExists=%v, defaultsApplied=%v, envVarsLoadedInContainer=%v", !configExists, defaultsApplied, envVarsLoaded && isInContainer))
698+ err = config.SaveConfig() // SaveConfig determines the correct path internally now
692699 if err != nil {
693- logger.Error("Failed to save configuration with environment variables", "error", err)
700+ // Log error but proceed, config is loaded, just potentially not saved
701+ logger.Error("LoadConfig: Failed to save final configuration", "error", err)
694702 } else {
695- logger.Info("Successfully updated config.yml with environment variable values")
703+ logger.Info("LoadConfig: Successfully saved final configuration")
696704 }
697705 }
698706
699- // Apply the log level from configuration
707+ // Apply the log level from the final configuration
700708 if config.General.LogLevel != "" {
701- // Set the log level using our centralized logger
702709 logger.GetLogger().SetLogLevel(config.General.LogLevel)
703- logger.Debug("Log level set from configuration", "level", config.General.LogLevel)
710+ logger.Debug("Log level set from final configuration", "level", config.General.LogLevel)
711+ } else {
712+ logger.Warn("Final configuration has empty LogLevel")
704713 }
705714
706- logger.Debug("LoadConfig: Completed LoadConfig function")
715+ // Final debug log before returning
716+ logger.Debug("LoadConfig: Completed interactive LoadConfig function")
707717 return config, nil
708718 }
709719
@@ -712,22 +722,41 @@ func (config *Config) SaveConfig() error {
712722 if err != nil {
713723 return fmt.Errorf("error getting configuration directory: %w", err)
714724 }
725+ isInContainer := docker.IsRunningInContainer()
715726
716- // Check for directly mounted config.yml in root when in container
727+ // Determine the correct save path
717728 var configFilePath string
718- if docker.IsRunningInContainer() && fileExists("/config.yml") {
719- configFilePath = "/config.yml"
729+ directMountPath := "/config.yml" // Use root path for direct container mount check
730+ if isInContainer && fileExists(directMountPath) {
731+ configFilePath = directMountPath
732+ logger.Debug("SaveConfig: Using direct mount path", "path", configFilePath)
733+ } else if isInContainer && fileExists("./config.yml") { // Check relative path if direct root mount check fails
734+ configFilePath = "./config.yml"
735+ logger.Debug("SaveConfig: Using relative path in container", "path", configFilePath)
720736 } else {
737+ // Default to XDG path if not a direct mount or relative path found in container
721738 configFilePath = filepath.Join(configDir, "config.yml")
739+ logger.Debug("SaveConfig: Using standard config directory path", "path", configFilePath)
740+
741+ // Ensure the directory exists before trying to write the file
742+ err = os.MkdirAll(filepath.Dir(configFilePath), 0755)
743+ if err != nil {
744+ return fmt.Errorf("error ensuring configuration directory exists for saving: %w", err)
745+ }
722746 }
723747
724748 logger.Info("Saving configuration to", "path", configFilePath)
725749
726- err = parser.WriteYAMLFile(configFilePath, config)
750+ // Create a temporary config copy excluding Build field for saving
751+ saveConfig := *config
752+ saveConfig.Build = BuildConfig{} // Exclude build info from saved file
753+
754+ err = parser.WriteYAMLFile(configFilePath, &saveConfig) // Pass pointer to the copy
727755 if err != nil {
728756 return fmt.Errorf("error writing configuration file: %w", err)
729757 }
730758
759+ logger.Debug("SaveConfig: Configuration saved successfully", "path", configFilePath)
731760 return nil
732761 }
733762
@@ -107,10 +107,13 @@ var (
107 detectUpstreamProxy = false // Default to disabled107 detectUpstreamProxy = false // Default to disabled
108 skipCertificates = false // Default to disabled108 skipCertificates = false // Default to disabled
109 enableRateLimit = false // Default to disabled109 enableRateLimit = false // Default to disabled
110+ enableHttpLogs = true // Default to enabled
110 defaultChallengeType = "http-01"111 defaultChallengeType = "http-01"
111 defaultHttpChallengePort = "80"112 defaultHttpChallengePort = "80"
112 defaultDnsChallengePropagationTimeout = 60 // seconds113 defaultDnsChallengePropagationTimeout = 60 // seconds
113 defaultDnsChallengePollingInterval = 5 // seconds114 defaultDnsChallengePollingInterval = 5 // seconds
115+ defaultAdminPath = "/admin"
116+ defaultNetwork = "gordon"
114 )117 )
115 118
116 // applyDefaultsToConfig applies default values to any fields that have zero values119 // applyDefaultsToConfig applies default values to any fields that have zero values
@@ -142,8 +145,8 @@ func applyDefaultsToConfig(config *Config) bool {
142 145
143 // Apply defaults to Admin config146 // Apply defaults to Admin config
144 if config.Admin.Path == "" {147 if config.Admin.Path == "" {
145- config.Admin.Path = "/admin"148+ config.Admin.Path = defaultAdminPath
146- logger.Debug("Applied default value for Admin.Path", "value", "/admin")149+ logger.Debug("Applied default value for Admin.Path", "value", defaultAdminPath)
147 defaultsApplied = true150 defaultsApplied = true
148 }151 }
149 152
@@ -159,8 +162,8 @@ func applyDefaultsToConfig(config *Config) bool {
159 defaultsApplied = true162 defaultsApplied = true
160 }163 }
161 if config.ContainerEngine.Network == "" {164 if config.ContainerEngine.Network == "" {
162- config.ContainerEngine.Network = "gordon"165+ config.ContainerEngine.Network = defaultNetwork
163- logger.Debug("Applied default value for ContainerEngine.Network", "value", "gordon")166+ logger.Debug("Applied default value for ContainerEngine.Network", "value", defaultNetwork)
164 defaultsApplied = true167 defaultsApplied = true
165 }168 }
166 169
@@ -208,8 +211,8 @@ func applyDefaultsToConfig(config *Config) bool {
208 if err == nil {211 if err == nil {
209 // Only set default if enableHttpLogs not explicitly specified212 // Only set default if enableHttpLogs not explicitly specified
210 if !strings.Contains(string(yamlContent), "enableHttpLogs:") {213 if !strings.Contains(string(yamlContent), "enableHttpLogs:") {
211- config.ReverseProxy.EnableHttpLogs = true214+ config.ReverseProxy.EnableHttpLogs = enableHttpLogs
212- logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", true)215+ logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", enableHttpLogs)
213 defaultsApplied = true216 defaultsApplied = true
214 } else {217 } else {
215 logger.Debug("Keeping explicit value for ReverseProxy.EnableHttpLogs", "value", config.ReverseProxy.EnableHttpLogs)218 logger.Debug("Keeping explicit value for ReverseProxy.EnableHttpLogs", "value", config.ReverseProxy.EnableHttpLogs)
@@ -217,21 +220,21 @@ func applyDefaultsToConfig(config *Config) bool {
217 } else {220 } else {
218 // If we can't read the file, apply the default only if not set221 // If we can't read the file, apply the default only if not set
219 if !config.ReverseProxy.EnableHttpLogs {222 if !config.ReverseProxy.EnableHttpLogs {
220- config.ReverseProxy.EnableHttpLogs = true223+ config.ReverseProxy.EnableHttpLogs = enableHttpLogs
221- logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", true)224+ logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", enableHttpLogs)
222 defaultsApplied = true225 defaultsApplied = true
223 }226 }
224 }227 }
225 } else {228 } else {
226 // If config file doesn't exist yet, set the default229 // If config file doesn't exist yet, set the default
227- config.ReverseProxy.EnableHttpLogs = true230+ config.ReverseProxy.EnableHttpLogs = enableHttpLogs
228- logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", true)231+ logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", enableHttpLogs)
229 defaultsApplied = true232 defaultsApplied = true
230 }233 }
231 // Set EnableRateLimit to false by default if not specified234 // Set EnableRateLimit to false by default if not specified
232 if !config.ReverseProxy.EnableRateLimit {235 if !config.ReverseProxy.EnableRateLimit {
233- config.ReverseProxy.EnableRateLimit = false236+ config.ReverseProxy.EnableRateLimit = enableRateLimit
234- logger.Debug("Applied default value for ReverseProxy.EnableRateLimit", "value", false)237+ logger.Debug("Applied default value for ReverseProxy.EnableRateLimit", "value", enableRateLimit)
235 defaultsApplied = true238 defaultsApplied = true
236 }239 }
237 240
@@ -249,6 +252,28 @@ func applyDefaultsToConfig(config *Config) bool {
249 defaultsApplied = true252 defaultsApplied = true
250 }253 }
251 254
255+ // Apply defaults for Let's Encrypt challenge settings
256+ if config.ReverseProxy.DefaultChallengeType == "" {
257+ config.ReverseProxy.DefaultChallengeType = defaultChallengeType
258+ logger.Debug("Applied default value for ReverseProxy.DefaultChallengeType", "value", defaultChallengeType)
259+ defaultsApplied = true
260+ }
261+ if config.ReverseProxy.DefaultHttpChallengePort == "" {
262+ config.ReverseProxy.DefaultHttpChallengePort = defaultHttpChallengePort
263+ logger.Debug("Applied default value for ReverseProxy.DefaultHttpChallengePort", "value", defaultHttpChallengePort)
264+ defaultsApplied = true
265+ }
266+ if config.ReverseProxy.DefaultDnsChallengePropagationTimeout == 0 {
267+ config.ReverseProxy.DefaultDnsChallengePropagationTimeout = defaultDnsChallengePropagationTimeout
268+ logger.Debug("Applied default value for ReverseProxy.DefaultDnsChallengePropagationTimeout", "value", defaultDnsChallengePropagationTimeout)
269+ defaultsApplied = true
270+ }
271+ if config.ReverseProxy.DefaultDnsChallengePollingInterval == 0 {
272+ config.ReverseProxy.DefaultDnsChallengePollingInterval = defaultDnsChallengePollingInterval
273+ logger.Debug("Applied default value for ReverseProxy.DefaultDnsChallengePollingInterval", "value", defaultDnsChallengePollingInterval)
274+ defaultsApplied = true
275+ }
276+
252 return defaultsApplied277 return defaultsApplied
253 }278 }
254 279
@@ -342,30 +367,6 @@ func getConfigDir() (string, error) {
342 var configDir string367 var configDir string
343 368
344 logger.Debug("getConfigDir: Checking if running in WSL")369 logger.Debug("getConfigDir: Checking if running in WSL")
345- if isWSL() {
346- logger.Debug("getConfigDir: Running in WSL")
347- // Use XDG_CONFIG_HOME for WSL
348- if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
349- logger.Debug("getConfigDir: Using XDG_CONFIG_HOME environment variable", "value", xdgConfigHome)
350- configDir = filepath.Join(xdgConfigHome, "Gordon")
351- } else {
352- // If XDG_CONFIG_HOME is not set, fall back to default locations
353- logger.Debug("getConfigDir: XDG_CONFIG_HOME not set, getting user home directory")
354- homeDir, err := os.UserHomeDir()
355- if err != nil {
356- // If we can't get the home directory, use a fallback
357- logger.Debug("getConfigDir: Failed to get user home directory, using temp dir", "error", err)
358- homeDir = os.TempDir() // Use the system's temp directory as a fallback
359- logger.Warn("Warning: Unable to determine home directory. Using temp directory", "dir", homeDir)
360- }
361-
362- configDir = filepath.Join(homeDir, ".config", "Gordon")
363- logger.Debug("getConfigDir: Set config directory based on home dir", "dir", configDir)
364- }
365-
366- logger.Debug("getConfigDir: Returning WSL config directory", "dir", configDir)
367- return configDir, nil
368- }
369 370
370 logger.Debug("getConfigDir: Checking if running in container")371 logger.Debug("getConfigDir: Checking if running in container")
371 if docker.IsRunningInContainer() {372 if docker.IsRunningInContainer() {
@@ -418,75 +419,106 @@ func (config *Config) LoadConfig() (*Config, error) {
418 // If we're in a non-interactive environment, use simplified config419 // If we're in a non-interactive environment, use simplified config
419 nonInteractiveEnv := os.Getenv("GORDON_NONINTERACTIVE")420 nonInteractiveEnv := os.Getenv("GORDON_NONINTERACTIVE")
420 isNonInteractive := nonInteractiveEnv == "true" || nonInteractiveEnv == "1" || nonInteractiveEnv == "yes"421 isNonInteractive := nonInteractiveEnv == "true" || nonInteractiveEnv == "1" || nonInteractiveEnv == "yes"
422+ isInContainer := docker.IsRunningInContainer() // Check once
423+
424+ // Flag to track if saving is needed at the end
425+ shouldSaveConfig := false
426+ defaultsApplied := false
421 427
422 if isNonInteractive {428 if isNonInteractive {
423 logger.Info("Non-interactive mode detected, using simplified config without prompts")429 logger.Info("Non-interactive mode detected, using simplified config without prompts")
424- // Create base configuration without prompts430+ // Start with an empty config
425- homeDir, err := os.UserHomeDir()431+ *config = Config{}
426- if err != nil {
427- homeDir = "/tmp"
428- logger.Warn("Could not get user home directory, using /tmp instead")
429- }
430 432
431- // Get default socket paths433+ // Determine StorageDir
432- sock := "/var/run/docker.sock"434+ if isInContainer {
433- podmansock := "/run/podman/podman.sock"435+ config.General.StorageDir = "/data"
436+ logger.Debug("Non-interactive: Using default container storage dir", "dir", config.General.StorageDir)
437+ } else {
438+ homeDir, err := os.UserHomeDir()
439+ if err != nil {
440+ homeDir = "/tmp"
441+ logger.Warn("Could not get user home directory, using /tmp instead")
442+ }
443+ config.General.StorageDir = filepath.Join(homeDir, ".gordon")
444+ logger.Debug("Non-interactive: Using default host storage dir", "dir", config.General.StorageDir)
445+ }
434 446
435- // Auto-detect Podman447+ // Auto-detect Podman and set relevant fields
436 isPodman, podmanSocket := docker.DetectPodman()448 isPodman, podmanSocket := docker.DetectPodman()
449+ config.ContainerEngine.Podman = isPodman
437 if isPodman {450 if isPodman {
438- podmansock = podmanSocket451+ config.ContainerEngine.PodmanSock = podmanSocket
452+ logger.Info("Non-interactive: Podman detected", "socket", podmanSocket)
453+ } else {
454+ // Explicitly set Docker sock if Podman not detected, applyDefaults might override if empty later
455+ config.ContainerEngine.Sock = sock
456+ logger.Info("Non-interactive: Assuming Docker")
439 }457 }
440 458
441- *config = Config{459+ // Apply remaining defaults
442- General: GeneralConfig{460+ defaultsApplied = applyDefaultsToConfig(config)
443- StorageDir: homeDir + "/.gordon",461+ if defaultsApplied {
444- LogLevel: defaultLogLevel,462+ logger.Info("Non-interactive: Applied default configuration values")
445- },463+ shouldSaveConfig = true // Mark for saving if defaults were needed
446- Http: HttpConfig{
447- Port: "8080",
448- Domain: "localhost",
449- Https: false,
450- },
451- Admin: AdminConfig{
452- Path: "/admin",
453- },
454- ContainerEngine: ContainerEngineConfig{
455- Sock: sock,
456- PodmanSock: podmansock,
457- Podman: isPodman, // Use the auto-detected value
458- Network: "gordon",
459- },
460- ReverseProxy: ReverseProxyConfig{
461- Port: reverseProxyPort,
462- HttpPort: httpPort,
463- CertDir: certDir,
464- RenewBefore: renewBefore,
465- LetsEncryptMode: letsEncryptMode,
466- GracePeriod: gracePeriod,
467- EnableHttpLogs: true,
468- EnableRateLimit: false,
469- },
470 }464 }
471 465
472- // If in container, apply env vars466+ // If in container, apply env vars (potentially overriding defaults)
473- if docker.IsRunningInContainer() {467+ if isInContainer {
474- logger.Info("Running in container, overriding configuration with environment variables...")468+ logger.Info("Non-interactive: Running in container, overriding configuration with environment variables...")
469+ // Assuming loadConfigFromEnv modifies config in place
470+ loadConfigFromEnv(config, !configLogsPrinted) // Pass flag to control initial logging
471+ configLogsPrinted = true // Mark logs as printed
472+ shouldSaveConfig = true // Mark for saving if env vars were applied
473+ logger.Debug("Non-interactive: Finished loading config from environment")
474+ }
475 475
476- loadConfigFromEnv(config, true)476+ // Load run env (common step)
477+ config.Build.RunEnv = os.Getenv("RUN_ENV")
478+ if config.Build.RunEnv == "" {
479+ config.Build.RunEnv = "prod"
480+ logger.Info("Non-interactive: No RUN_ENV specified, defaulting to 'prod'")
481+ } else {
482+ logger.Info("Non-interactive: Run environment set", "env", config.Build.RunEnv)
477 }483 }
478 484
479- return config, nil485+ // Check AdminWebUI (common step)
486+ if config.Admin.Path == "" {
487+ logger.Warn("Non-interactive: Admin path is empty or not set, admin dashboard webui will be disabled")
488+ config.Admin.AdminWebUI = false
489+ } else {
490+ config.Admin.AdminWebUI = true
491+ }
492+
493+ // Apply log level (common step)
494+ if config.General.LogLevel != "" {
495+ logger.GetLogger().SetLogLevel(config.General.LogLevel)
496+ logger.Debug("Non-interactive: Log level set from configuration", "level", config.General.LogLevel)
497+ }
498+
499+ // Save config if needed (only in non-interactive mode if defaults/env applied)
500+ if shouldSaveConfig {
501+ logger.Info("Non-interactive: Saving configuration...")
502+ err := config.SaveConfig()
503+ if err != nil {
504+ // Log error but continue, as config is loaded, just not saved
505+ logger.Error("Non-interactive: Failed to save configuration", "error", err)
506+ } else {
507+ logger.Info("Non-interactive: Successfully saved configuration")
508+ }
509+ }
510+
511+ logger.Debug("LoadConfig: Completed non-interactive setup")
512+ return config, nil // Return early for non-interactive mode
480 }513 }
481 514
482- // Original LoadConfig implementation continues below for interactive mode515+ // ---- Interactive Mode Logic ----
483- logger.Debug("LoadConfig: Starting LoadConfig function")516+ logger.Debug("LoadConfig: Starting interactive LoadConfig function")
484 517
485 // Log current execution environment for debugging518 // Log current execution environment for debugging
486- isInContainer := docker.IsRunningInContainer()
487 cwd, _ := os.Getwd()519 cwd, _ := os.Getwd()
488 logger.Debug("LoadConfig: Environment information",520 logger.Debug("LoadConfig: Environment information",
489- "isContainer", isInContainer,521+ "isContainer", isInContainer, // Use cached value
490 "currentDir", cwd,522 "currentDir", cwd,
491 "ENV", os.Getenv("ENV"),523 "ENV", os.Getenv("ENV"),
492 "GORDON_NONINTERACTIVE", os.Getenv("GORDON_NONINTERACTIVE"))524 "GORDON_NONINTERACTIVE", os.Getenv("GORDON_NONINTERACTIVE"))
@@ -510,11 +542,15 @@ func (config *Config) LoadConfig() (*Config, error) {
510 logger.Debug("LoadConfig: Got config directory", "dir", configDir)542 logger.Debug("LoadConfig: Got config directory", "dir", configDir)
511 logger.Info("Using configuration directory", "dir", configDir)543 logger.Info("Using configuration directory", "dir", configDir)
512 544
513- // Check for directly mounted config.yml in root when in container545+ // Determine config file path
514 var configFilePath string546 var configFilePath string
515- logger.Debug("LoadConfig: Checking if running in container")547+ logger.Debug("LoadConfig: Checking config file location")
516- if docker.IsRunningInContainer() && fileExists("./config.yml") {548+ // Check for direct mount first only if in container
517- configFilePath = "./config.yml"549+ directMountPath := "./config.yml"
550+ useDirectMount := isInContainer && fileExists(directMountPath)
551+
552+ if useDirectMount {
553+ configFilePath = directMountPath
518 logger.Info("Found directly mounted config.yml in container current directory")554 logger.Info("Found directly mounted config.yml in container current directory")
519 } else {555 } else {
520 configFilePath = filepath.Join(configDir, "config.yml")556 configFilePath = filepath.Join(configDir, "config.yml")
@@ -526,43 +562,50 @@ func (config *Config) LoadConfig() (*Config, error) {
526 logger.Info("Reloading configuration (already loaded previously)...")562 logger.Info("Reloading configuration (already loaded previously)...")
527 }563 }
528 564
529- configExists := true565+ configExists := true // Assume exists unless stat fails
530- var defaultsApplied bool = false
531 566
532 logger.Debug("LoadConfig: Checking if config file exists", "path", configFilePath)567 logger.Debug("LoadConfig: Checking if config file exists", "path", configFilePath)
533 _, err = os.Stat(configFilePath)568 _, err = os.Stat(configFilePath)
534 if errors.Is(err, fs.ErrNotExist) {569 if errors.Is(err, fs.ErrNotExist) {
535 configExists = false570 configExists = false
571+ shouldSaveConfig = true // Need to save if we create it
536 logger.Info("Config file not found, creating it", "path", configFilePath)572 logger.Info("Config file not found, creating it", "path", configFilePath)
537 573
538- // Create config dir if it doesn't exist574+ // Create config dir if it doesn't exist (only needed if using configDir path)
539- logger.Debug("LoadConfig: Creating config directory", "dir", configDir)575+ if !useDirectMount {
540- err = os.MkdirAll(configDir, 0755)576+ logger.Debug("LoadConfig: Creating config directory", "dir", configDir)
541- if err != nil {577+ err = os.MkdirAll(configDir, 0755)
542- logger.Error("LoadConfig: Failed to create config directory", "error", err)578+ if err != nil {
543- return nil, fmt.Errorf("error creating configuration directory: %w", err)579+ logger.Error("LoadConfig: Failed to create config directory", "error", err)
580+ return nil, fmt.Errorf("error creating configuration directory: %w", err)
581+ }
582+ } else {
583+ logger.Debug("LoadConfig: Using direct mount, skipping config directory creation")
544 }584 }
545 585
546- // Create config file with the default values of ContainerEngineConfig586+ // Start with an empty config
547- logger.Debug("LoadConfig: Getting user home directory")587+ *config = Config{}
548- homeDir, err := os.UserHomeDir()
549- if err != nil {
550- logger.Error("LoadConfig: Failed to get user home directory", "error", err)
551- return nil, fmt.Errorf("could not get user home directory: %w", err)
552- }
553 588
554- // Check if running in container before creating the config589+ // Set specific initial values before applying defaults
555- logger.Debug("LoadConfig: Checking if running in container for podman setting")
556- isInContainer := docker.IsRunningInContainer()
557- logger.Info("Is running in container", "value", isInContainer)
558 590
559- // Set default podman value based on environment591+ // Storage Directory
560- defaultPodman := false592+ if isInContainer {
561- // Check for non-interactive mode flag593+ config.General.StorageDir = "/data"
562- nonInteractive := os.Getenv("GORDON_NONINTERACTIVE") == "true"594+ logger.Debug("LoadConfig: Setting initial StorageDir for container", "value", config.General.StorageDir)
595+ } else {
596+ homeDir, err := os.UserHomeDir()
597+ if err != nil {
598+ logger.Error("LoadConfig: Failed to get user home directory", "error", err)
599+ // Setting a temporary default, applyDefaultsToConfig will handle it properly later if needed
600+ homeDir = "/tmp"
601+ }
602+ config.General.StorageDir = filepath.Join(homeDir, ".gordon")
603+ logger.Debug("LoadConfig: Setting initial StorageDir for host", "value", config.General.StorageDir)
604+ }
563 605
564- // Only prompt if not in a container and not in non-interactive mode606+ // Podman Setting
565- if !isInContainer && !nonInteractive {607+ var defaultPodman bool
608+ if !isInContainer { // Only prompt if interactive and not in a container
566 logger.Info("Not in container, prompting for podman preference...")609 logger.Info("Not in container, prompting for podman preference...")
567 logger.Debug("LoadConfig: About to prompt for podman preference")610 logger.Debug("LoadConfig: About to prompt for podman preference")
568 defaultPodman = ReadUserInputNonBlocking("Are you using podman ? (y/n)", "n") == "y"611 defaultPodman = ReadUserInputNonBlocking("Are you using podman ? (y/n)", "n") == "y"
@@ -571,43 +614,18 @@ func (config *Config) LoadConfig() (*Config, error) {
571 defaultPodman = true614 defaultPodman = true
572 logger.Info("Podman container engine detected via environment variable")615 logger.Info("Podman container engine detected via environment variable")
573 } else {616 } else {
617+ // Default to false (Docker) in container if env var not set
618+ defaultPodman = false
574 logger.Info("Using Docker as the default container engine in container environment")619 logger.Info("Using Docker as the default container engine in container environment")
575 }620 }
621+ config.ContainerEngine.Podman = defaultPodman
622+ logger.Debug("LoadConfig: Set initial podman value", "value", defaultPodman)
576 623
577- logger.Debug("LoadConfig: Creating default config structure")624+ // Now apply all other defaults
578- *config = Config{625+ logger.Info("Applying default values for the new configuration...")
579- General: GeneralConfig{626+ defaultsApplied = applyDefaultsToConfig(config)
580- StorageDir: homeDir + "/.gordon",627+ // defaultsApplied is already true implicitly because we created the file
581- LogLevel: defaultLogLevel,
582- },
583- Http: HttpConfig{
584- Port: "8080",
585- Domain: "localhost",
586- Https: true,
587- },
588- Admin: AdminConfig{
589- Path: "/admin",
590- },
591- ContainerEngine: ContainerEngineConfig{
592- Sock: sock,
593- PodmanSock: podmansock,
594- Podman: defaultPodman,
595- Network: "gordon",
596- },
597- ReverseProxy: ReverseProxyConfig{
598- Port: reverseProxyPort,
599- HttpPort: httpPort,
600- CertDir: certDir,
601- RenewBefore: renewBefore,
602- LetsEncryptMode: letsEncryptMode,
603- GracePeriod: gracePeriod,
604- EnableHttpLogs: true,
605- EnableRateLimit: false,
606- },
607- }
608 628
609- // In this case, all defaults were applied since we created a new config
610- defaultsApplied = true
611 } else if err != nil {629 } else if err != nil {
612 logger.Error("LoadConfig: Error checking if config file exists", "error", err)630 logger.Error("LoadConfig: Error checking if config file exists", "error", err)
613 return nil, fmt.Errorf("error checking configuration file: %w", err)631 return nil, fmt.Errorf("error checking configuration file: %w", err)
@@ -615,7 +633,17 @@ func (config *Config) LoadConfig() (*Config, error) {
615 // If config file exists, read it633 // If config file exists, read it
616 logger.Info("Reading existing configuration file...")634 logger.Info("Reading existing configuration file...")
617 logger.Debug("LoadConfig: About to read and unmarshal existing config file")635 logger.Debug("LoadConfig: About to read and unmarshal existing config file")
618- err = readAndUnmarshalConfig(os.DirFS(filepath.Dir(configFilePath)), filepath.Base(configFilePath), config)636+ // Use DirFS based on whether it's a direct mount or in configDir
637+ var baseDir string
638+ var fileName string
639+ if useDirectMount {
640+ baseDir = "."
641+ fileName = "config.yml"
642+ } else {
643+ baseDir = configDir
644+ fileName = "config.yml"
645+ }
646+ err = readAndUnmarshalConfig(os.DirFS(baseDir), fileName, config)
619 if err != nil {647 if err != nil {
620 logger.Error("LoadConfig: Failed to read/unmarshal config file", "error", err)648 logger.Error("LoadConfig: Failed to read/unmarshal config file", "error", err)
621 return nil, err649 return nil, err
@@ -625,58 +653,37 @@ func (config *Config) LoadConfig() (*Config, error) {
625 // Apply default values to fields that were not specified in the config file653 // Apply default values to fields that were not specified in the config file
626 logger.Info("Applying default values to missing configuration fields...")654 logger.Info("Applying default values to missing configuration fields...")
627 defaultsApplied = applyDefaultsToConfig(config)655 defaultsApplied = applyDefaultsToConfig(config)
656+ if defaultsApplied {
657+ shouldSaveConfig = true // Mark for saving if defaults were applied to existing file
658+ }
628 }659 }
629 660
630- // Only load environment variables once, after either creating a new config or reading an existing one661+ // --- Common Steps for Interactive Mode (after file read/creation) ---
631- logger.Debug("LoadConfig: Checking if running in container for env vars")662+
632- if docker.IsRunningInContainer() {663+ // Load environment variables if in a container (might override file/defaults)
633- // Only print logs if we haven't printed them before664+ envVarsLoaded := false
665+ if isInContainer {
634 shouldPrintLogs := !configLogsPrinted666 shouldPrintLogs := !configLogsPrinted
635 logger.Info("Running in container, overriding configuration with environment variables...")667 logger.Info("Running in container, overriding configuration with environment variables...")
636 logger.Debug("LoadConfig: About to load config from environment")668 logger.Debug("LoadConfig: About to load config from environment")
637- loadConfigFromEnv(config, shouldPrintLogs)669+ loadConfigFromEnv(config, shouldPrintLogs) // Assuming modification in place
670+ envVarsLoaded = true
638 configLogsPrinted = true671 configLogsPrinted = true
672+ shouldSaveConfig = true // Mark for saving if env vars were applied
639 logger.Debug("LoadConfig: Finished loading config from environment")673 logger.Debug("LoadConfig: Finished loading config from environment")
640 }674 }
641 675
642 // Load run env676 // Load run env
643 logger.Debug("LoadConfig: Setting run environment")677 logger.Debug("LoadConfig: Setting run environment")
644 config.Build.RunEnv = os.Getenv("RUN_ENV")678 config.Build.RunEnv = os.Getenv("RUN_ENV")
645- logger.Info("Run environment", "env", config.Build.RunEnv)
646-
647 if config.Build.RunEnv == "" {679 if config.Build.RunEnv == "" {
648 config.Build.RunEnv = "prod"680 config.Build.RunEnv = "prod"
649 logger.Info("No RUN_ENV specified, defaulting to 'prod'")681 logger.Info("No RUN_ENV specified, defaulting to 'prod'")
650- }
651-
652- // If we created a new config or applied defaults to an existing one, save it
653- if !configExists || defaultsApplied {
654- logger.Info("Saving configuration with default values...")
655- logger.Debug("LoadConfig: About to save configuration with defaults")
656- err = config.SaveConfig()
657- if err != nil {
658- logger.Error("LoadConfig: Failed to save configuration", "error", err)
659- return nil, fmt.Errorf("error saving configuration: %w", err)
660- }
661- logger.Debug("LoadConfig: Successfully saved configuration with defaults")
662- }
663-
664- // Debug output to verify config was loaded correctly
665- // Only print this debug information if we're also printing the env var logs
666- if !configLogsPrinted {
667- logger.Info("Loaded configuration from", "path", configFilePath)
668- logger.Info("Container engine config",
669- "docker_socket", config.ContainerEngine.Sock,
670- "podman_socket", config.ContainerEngine.PodmanSock,
671- "using_podman", config.ContainerEngine.Podman,
672- "network", config.ContainerEngine.Network)
673 } else {682 } else {
674- // Just print a simplified message683+ logger.Info("Run environment set", "env", config.Build.RunEnv)
675- logger.Info("Loaded configuration from", "path", configFilePath)
676 }684 }
677 685
678- // After loading from file or environment, check if Admin.Path is empty686+ // Check AdminWebUI status
679- // This should be placed before returning the config
680 if config.Admin.Path == "" {687 if config.Admin.Path == "" {
681 logger.Warn("Admin path is empty or not set, admin dashboard webui will be disabled")688 logger.Warn("Admin path is empty or not set, admin dashboard webui will be disabled")
682 config.Admin.AdminWebUI = false689 config.Admin.AdminWebUI = false
@@ -684,26 +691,29 @@ func (config *Config) LoadConfig() (*Config, error) {
684 config.Admin.AdminWebUI = true691 config.Admin.AdminWebUI = true
685 }692 }
686 693
687- // Save config file if running in container and env vars were applied694+ // Save config file if it was created, defaults were applied, or env vars loaded in container
688- // This ensures config.yml always reflects the environment variables695+ if shouldSaveConfig {
689- if docker.IsRunningInContainer() && configLogsPrinted {696+ logger.Info("Saving final configuration state...", "path", configFilePath)
690- logger.Info("Environment variables were applied, saving configuration to reflect current values...")697+ logger.Debug("LoadConfig: About to save configuration", "reason", fmt.Sprintf("configExists=%v, defaultsApplied=%v, envVarsLoadedInContainer=%v", !configExists, defaultsApplied, envVarsLoaded && isInContainer))
691- err = config.SaveConfig()698+ err = config.SaveConfig() // SaveConfig determines the correct path internally now
692 if err != nil {699 if err != nil {
693- logger.Error("Failed to save configuration with environment variables", "error", err)700+ // Log error but proceed, config is loaded, just potentially not saved
701+ logger.Error("LoadConfig: Failed to save final configuration", "error", err)
694 } else {702 } else {
695- logger.Info("Successfully updated config.yml with environment variable values")703+ logger.Info("LoadConfig: Successfully saved final configuration")
696 }704 }
697 }705 }
698 706
699- // Apply the log level from configuration707+ // Apply the log level from the final configuration
700 if config.General.LogLevel != "" {708 if config.General.LogLevel != "" {
701- // Set the log level using our centralized logger
702 logger.GetLogger().SetLogLevel(config.General.LogLevel)709 logger.GetLogger().SetLogLevel(config.General.LogLevel)
703- logger.Debug("Log level set from configuration", "level", config.General.LogLevel)710+ logger.Debug("Log level set from final configuration", "level", config.General.LogLevel)
711+ } else {
712+ logger.Warn("Final configuration has empty LogLevel")
704 }713 }
705 714
706- logger.Debug("LoadConfig: Completed LoadConfig function")715+ // Final debug log before returning
716+ logger.Debug("LoadConfig: Completed interactive LoadConfig function")
707 return config, nil717 return config, nil
708 }718 }
709 719
@@ -712,22 +722,41 @@ func (config *Config) SaveConfig() error {
712 if err != nil {722 if err != nil {
713 return fmt.Errorf("error getting configuration directory: %w", err)723 return fmt.Errorf("error getting configuration directory: %w", err)
714 }724 }
725+ isInContainer := docker.IsRunningInContainer()
715 726
716- // Check for directly mounted config.yml in root when in container727+ // Determine the correct save path
717 var configFilePath string728 var configFilePath string
718- if docker.IsRunningInContainer() && fileExists("/config.yml") {729+ directMountPath := "/config.yml" // Use root path for direct container mount check
719- configFilePath = "/config.yml"730+ if isInContainer && fileExists(directMountPath) {
731+ configFilePath = directMountPath
732+ logger.Debug("SaveConfig: Using direct mount path", "path", configFilePath)
733+ } else if isInContainer && fileExists("./config.yml") { // Check relative path if direct root mount check fails
734+ configFilePath = "./config.yml"
735+ logger.Debug("SaveConfig: Using relative path in container", "path", configFilePath)
720 } else {736 } else {
737+ // Default to XDG path if not a direct mount or relative path found in container
721 configFilePath = filepath.Join(configDir, "config.yml")738 configFilePath = filepath.Join(configDir, "config.yml")
739+ logger.Debug("SaveConfig: Using standard config directory path", "path", configFilePath)
740+
741+ // Ensure the directory exists before trying to write the file
742+ err = os.MkdirAll(filepath.Dir(configFilePath), 0755)
743+ if err != nil {
744+ return fmt.Errorf("error ensuring configuration directory exists for saving: %w", err)
745+ }
722 }746 }
723 747
724 logger.Info("Saving configuration to", "path", configFilePath)748 logger.Info("Saving configuration to", "path", configFilePath)
725 749
726- err = parser.WriteYAMLFile(configFilePath, config)750+ // Create a temporary config copy excluding Build field for saving
751+ saveConfig := *config
752+ saveConfig.Build = BuildConfig{} // Exclude build info from saved file
753+
754+ err = parser.WriteYAMLFile(configFilePath, &saveConfig) // Pass pointer to the copy
727 if err != nil {755 if err != nil {
728 return fmt.Errorf("error writing configuration file: %w", err)756 return fmt.Errorf("error writing configuration file: %w", err)
729 }757 }
730 758
759+ logger.Debug("SaveConfig: Configuration saved successfully", "path", configFilePath)
731 return nil760 return nil
732 }761 }
733 762
M internal/db/queries/proxy.go +3 -0
@@ -127,10 +127,13 @@ func NewProxyQueries() *ProxyQueries {
127127 WHERE name = ?
128128 `,
129129 GetCertificateByDomain: `
130+ -- Reverted: Select all original columns as expected by checkExistingCertificate
130131 SELECT c.cert_file, c.key_file, c.issued_at, c.expires_at, c.issuer, c.status
131132 FROM certificate c
132133 JOIN domain d ON c.domain_id = d.id
133134 WHERE d.name = ?
135+ -- Note: This query might return multiple rows or non-valid certs.
136+ -- The calling code (checkExistingCertificate and GetCertificate) needs to handle this.
134137 `,
135138 UpdateCertificate: `
136139 INSERT OR REPLACE INTO certificate (
@@ -127,10 +127,13 @@ func NewProxyQueries() *ProxyQueries {
127 WHERE name = ?127 WHERE name = ?
128 `,128 `,
129 GetCertificateByDomain: `129 GetCertificateByDomain: `
130+ -- Reverted: Select all original columns as expected by checkExistingCertificate
130 SELECT c.cert_file, c.key_file, c.issued_at, c.expires_at, c.issuer, c.status131 SELECT c.cert_file, c.key_file, c.issued_at, c.expires_at, c.issuer, c.status
131 FROM certificate c132 FROM certificate c
132 JOIN domain d ON c.domain_id = d.id133 JOIN domain d ON c.domain_id = d.id
133 WHERE d.name = ?134 WHERE d.name = ?
135+ -- Note: This query might return multiple rows or non-valid certs.
136+ -- The calling code (checkExistingCertificate and GetCertificate) needs to handle this.
134 `,137 `,
135 UpdateCertificate: `138 UpdateCertificate: `
136 INSERT OR REPLACE INTO certificate (139 INSERT OR REPLACE INTO certificate (
M internal/proxy/acme.go +75 -14
@@ -96,6 +96,8 @@ func NewCertificateManager(config CertManagerConfig, app interfaces.AppInterface
9696 logger.Error("Failed to save newly generated ACME user key to DB", "error", saveErr)
9797 // Depending on policy, might want to return error here:
9898 // return nil, fmt.Errorf("failed to save initial ACME user: %w", saveErr)
99+ } else {
100+ logger.Debug("Saved ACME user details to DB", "email", user.Email)
99101 }
100102 } else {
101103 // Handle other DB errors during load
@@ -202,6 +204,9 @@ func loadAcmeUserFromDB(email string, db *sql.DB, queries *queries.ProxyQueries)
202204 return nil, fmt.Errorf("failed to query ACME user: %w", err)
203205 }
204206
207+ // Add detailed logging for the loaded registration info
208+ logger.Debug("Raw registration_info from DB", "email", email, "Valid", registrationJSON.Valid, "String", registrationJSON.String)
209+
205210 // Parse the private key
206211 privateKey, err := certcrypto.ParsePEMPrivateKey([]byte(privateKeyPEM))
207212 if err != nil {
@@ -482,32 +487,88 @@ func (cm *CertificateManager) RenewCertificate(domainName string, proxy *Proxy)
482487 func (cm *CertificateManager) GetCertificateForTLS(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
483488 logger.Debug("Getting certificate for TLS handshake", "server_name", hello.ServerName)
484489
485- // Get the certificate resource
486- cert, err := cm.GetCertificate(hello.ServerName)
490+ // Get the certificate resource from the database
491+ certResource, err := cm.GetCertificate(hello.ServerName)
487492 if err != nil {
488- return nil, fmt.Errorf("failed to get certificate: %w", err)
493+ // If the certificate is specifically not found in the DB,
494+ // return nil, nil to indicate no certificate is available for this ServerName.
495+ // This allows the TLS handshake to potentially use other certificates or fail gracefully.
496+ if err == sql.ErrNoRows {
497+ logger.Debug("Certificate not found in DB during TLS handshake", "server_name", hello.ServerName)
498+ return nil, nil // Indicate no certificate available for this name
499+ }
500+ // For other errors (DB connection issues, etc.), return the error.
501+ logger.Error("Error getting certificate during TLS handshake", "server_name", hello.ServerName, "error", err)
502+ return nil, fmt.Errorf("failed to get certificate for %s: %w", hello.ServerName, err)
489503 }
490504
491- // Convert to tls.Certificate
492- tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
505+ // Convert the retrieved certificate resource to tls.Certificate
506+ tlsCert, err := tls.X509KeyPair(certResource.Certificate, certResource.PrivateKey)
493507 if err != nil {
494- return nil, fmt.Errorf("failed to parse certificate: %w", err)
508+ logger.Error("Failed to create key pair from retrieved certificate", "server_name", hello.ServerName, "error", err)
509+ return nil, fmt.Errorf("failed to parse retrieved certificate for %s: %w", hello.ServerName, err)
495510 }
496511
497512 return &tlsCert, nil
498513 }
499514
500-// GetCertificate retrieves a certificate from the database
515+// GetCertificate retrieves a certificate for a domain from the database
501516 func (cm *CertificateManager) GetCertificate(domainName string) (*certificate.Resource, error) {
502- // Try to obtain the certificate if it doesn't exist
503- request := certificate.ObtainRequest{
504- Domains: []string{domainName},
505- Bundle: true,
506- }
507- cert, err := cm.client.Certificate.Obtain(request)
517+ logger.Debug("Attempting to retrieve certificate from DB", "domain", domainName)
518+ var certPEM, keyPEM string
519+ // Add dummy variables to scan the extra columns we don't need in this function
520+ var issuedAt, expiresAt, issuer, status sql.NullString // Use sql.NullString or appropriate types
521+
522+ // Query the database using the predefined query (which now returns 6 columns)
523+ err := cm.db.QueryRow(cm.queries.GetCertificateByDomain, domainName).Scan(
524+ &certPEM, &keyPEM, // The columns we need
525+ &issuedAt, &expiresAt, &issuer, &status, // Dummy scans for the rest
526+ )
527+
508528 if err != nil {
509- return nil, fmt.Errorf("failed to obtain certificate: %w", err)
529+ // Check for ErrNoRows *after* attempting the scan
530+ if err == sql.ErrNoRows {
531+ logger.Warn("Certificate not found in DB for domain, attempting to obtain.", "domain", domainName)
532+ // Certificate not found, try obtaining a new one.
533+ // Note: Need the 'proxy' instance here. This function signature might need adjustment,
534+ // or the obtaining logic should be handled higher up (e.g., in GetCertificateForTLS or the proxy core).
535+ // For now, returning an error to highlight this dependency.
536+ // Alternatively, we could pass the proxy instance or necessary config down.
537+ // Let's return a specific error indicating it needs obtaining.
538+ return nil, fmt.Errorf("certificate not found in DB for domain %s, obtain needed", domainName)
539+
540+ // --- Alternative: Obtain directly (Requires Proxy instance or refactor) ---
541+ // proxyInstance := ??? // How to get the proxy instance here?
542+ // if proxyInstance == nil {
543+ // return nil, fmt.Errorf("cannot obtain certificate without proxy instance in GetCertificate")
544+ // }
545+ // obtainedCert, obtainErr := cm.ObtainCertificate(domainName, proxyInstance)
546+ // if obtainErr != nil {
547+ // logger.Error("Failed to obtain certificate after not finding in DB", "domain", domainName, "error", obtainErr)
548+ // return nil, fmt.Errorf("certificate not found and obtain failed for %s: %w", domainName, obtainErr)
549+ // }
550+ // logger.Info("Successfully obtained certificate after not finding in DB", "domain", domainName)
551+ // return obtainedCert, nil
552+ // --- End Alternative ---
553+
554+ }
555+ logger.Error("Failed to query certificate from DB", "domain", domainName, "error", err)
556+ return nil, fmt.Errorf("database error retrieving certificate for %s: %w", domainName, err)
510557 }
558+
559+ logger.Debug("Successfully retrieved certificate from DB", "domain", domainName)
560+
561+ // Construct the certificate resource
562+ cert := &certificate.Resource{
563+ Domain: domainName,
564+ CertURL: "", // Not stored/retrieved from DB in this example
565+ CertStableURL: "", // Not stored/retrieved from DB
566+ PrivateKey: []byte(keyPEM),
567+ Certificate: []byte(certPEM),
568+ IssuerCertificate: nil, // Not typically stored/needed for serving
569+ CSR: nil, // Not stored
570+ }
571+
511572 return cert, nil
512573 }
513574
@@ -96,6 +96,8 @@ func NewCertificateManager(config CertManagerConfig, app interfaces.AppInterface
96 logger.Error("Failed to save newly generated ACME user key to DB", "error", saveErr)96 logger.Error("Failed to save newly generated ACME user key to DB", "error", saveErr)
97 // Depending on policy, might want to return error here:97 // Depending on policy, might want to return error here:
98 // return nil, fmt.Errorf("failed to save initial ACME user: %w", saveErr)98 // return nil, fmt.Errorf("failed to save initial ACME user: %w", saveErr)
99+ } else {
100+ logger.Debug("Saved ACME user details to DB", "email", user.Email)
99 }101 }
100 } else {102 } else {
101 // Handle other DB errors during load103 // Handle other DB errors during load
@@ -202,6 +204,9 @@ func loadAcmeUserFromDB(email string, db *sql.DB, queries *queries.ProxyQueries)
202 return nil, fmt.Errorf("failed to query ACME user: %w", err)204 return nil, fmt.Errorf("failed to query ACME user: %w", err)
203 }205 }
204 206
207+ // Add detailed logging for the loaded registration info
208+ logger.Debug("Raw registration_info from DB", "email", email, "Valid", registrationJSON.Valid, "String", registrationJSON.String)
209+
205 // Parse the private key210 // Parse the private key
206 privateKey, err := certcrypto.ParsePEMPrivateKey([]byte(privateKeyPEM))211 privateKey, err := certcrypto.ParsePEMPrivateKey([]byte(privateKeyPEM))
207 if err != nil {212 if err != nil {
@@ -482,32 +487,88 @@ func (cm *CertificateManager) RenewCertificate(domainName string, proxy *Proxy)
482 func (cm *CertificateManager) GetCertificateForTLS(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {487 func (cm *CertificateManager) GetCertificateForTLS(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
483 logger.Debug("Getting certificate for TLS handshake", "server_name", hello.ServerName)488 logger.Debug("Getting certificate for TLS handshake", "server_name", hello.ServerName)
484 489
485- // Get the certificate resource490+ // Get the certificate resource from the database
486- cert, err := cm.GetCertificate(hello.ServerName)491+ certResource, err := cm.GetCertificate(hello.ServerName)
487 if err != nil {492 if err != nil {
488- return nil, fmt.Errorf("failed to get certificate: %w", err)493+ // If the certificate is specifically not found in the DB,
494+ // return nil, nil to indicate no certificate is available for this ServerName.
495+ // This allows the TLS handshake to potentially use other certificates or fail gracefully.
496+ if err == sql.ErrNoRows {
497+ logger.Debug("Certificate not found in DB during TLS handshake", "server_name", hello.ServerName)
498+ return nil, nil // Indicate no certificate available for this name
499+ }
500+ // For other errors (DB connection issues, etc.), return the error.
501+ logger.Error("Error getting certificate during TLS handshake", "server_name", hello.ServerName, "error", err)
502+ return nil, fmt.Errorf("failed to get certificate for %s: %w", hello.ServerName, err)
489 }503 }
490 504
491- // Convert to tls.Certificate505+ // Convert the retrieved certificate resource to tls.Certificate
492- tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)506+ tlsCert, err := tls.X509KeyPair(certResource.Certificate, certResource.PrivateKey)
493 if err != nil {507 if err != nil {
494- return nil, fmt.Errorf("failed to parse certificate: %w", err)508+ logger.Error("Failed to create key pair from retrieved certificate", "server_name", hello.ServerName, "error", err)
509+ return nil, fmt.Errorf("failed to parse retrieved certificate for %s: %w", hello.ServerName, err)
495 }510 }
496 511
497 return &tlsCert, nil512 return &tlsCert, nil
498 }513 }
499 514
500-// GetCertificate retrieves a certificate from the database515+// GetCertificate retrieves a certificate for a domain from the database
501 func (cm *CertificateManager) GetCertificate(domainName string) (*certificate.Resource, error) {516 func (cm *CertificateManager) GetCertificate(domainName string) (*certificate.Resource, error) {
502- // Try to obtain the certificate if it doesn't exist517+ logger.Debug("Attempting to retrieve certificate from DB", "domain", domainName)
503- request := certificate.ObtainRequest{518+ var certPEM, keyPEM string
504- Domains: []string{domainName},519+ // Add dummy variables to scan the extra columns we don't need in this function
505- Bundle: true,520+ var issuedAt, expiresAt, issuer, status sql.NullString // Use sql.NullString or appropriate types
506- }521+
507- cert, err := cm.client.Certificate.Obtain(request)522+ // Query the database using the predefined query (which now returns 6 columns)
523+ err := cm.db.QueryRow(cm.queries.GetCertificateByDomain, domainName).Scan(
524+ &certPEM, &keyPEM, // The columns we need
525+ &issuedAt, &expiresAt, &issuer, &status, // Dummy scans for the rest
526+ )
527+
508 if err != nil {528 if err != nil {
509- return nil, fmt.Errorf("failed to obtain certificate: %w", err)529+ // Check for ErrNoRows *after* attempting the scan
530+ if err == sql.ErrNoRows {
531+ logger.Warn("Certificate not found in DB for domain, attempting to obtain.", "domain", domainName)
532+ // Certificate not found, try obtaining a new one.
533+ // Note: Need the 'proxy' instance here. This function signature might need adjustment,
534+ // or the obtaining logic should be handled higher up (e.g., in GetCertificateForTLS or the proxy core).
535+ // For now, returning an error to highlight this dependency.
536+ // Alternatively, we could pass the proxy instance or necessary config down.
537+ // Let's return a specific error indicating it needs obtaining.
538+ return nil, fmt.Errorf("certificate not found in DB for domain %s, obtain needed", domainName)
539+
540+ // --- Alternative: Obtain directly (Requires Proxy instance or refactor) ---
541+ // proxyInstance := ??? // How to get the proxy instance here?
542+ // if proxyInstance == nil {
543+ // return nil, fmt.Errorf("cannot obtain certificate without proxy instance in GetCertificate")
544+ // }
545+ // obtainedCert, obtainErr := cm.ObtainCertificate(domainName, proxyInstance)
546+ // if obtainErr != nil {
547+ // logger.Error("Failed to obtain certificate after not finding in DB", "domain", domainName, "error", obtainErr)
548+ // return nil, fmt.Errorf("certificate not found and obtain failed for %s: %w", domainName, obtainErr)
549+ // }
550+ // logger.Info("Successfully obtained certificate after not finding in DB", "domain", domainName)
551+ // return obtainedCert, nil
552+ // --- End Alternative ---
553+
554+ }
555+ logger.Error("Failed to query certificate from DB", "domain", domainName, "error", err)
556+ return nil, fmt.Errorf("database error retrieving certificate for %s: %w", domainName, err)
510 }557 }
558+
559+ logger.Debug("Successfully retrieved certificate from DB", "domain", domainName)
560+
561+ // Construct the certificate resource
562+ cert := &certificate.Resource{
563+ Domain: domainName,
564+ CertURL: "", // Not stored/retrieved from DB in this example
565+ CertStableURL: "", // Not stored/retrieved from DB
566+ PrivateKey: []byte(keyPEM),
567+ Certificate: []byte(certPEM),
568+ IssuerCertificate: nil, // Not typically stored/needed for serving
569+ CSR: nil, // Not stored
570+ }
571+
511 return cert, nil572 return cert, nil
512 }573 }
513 574
M internal/proxy/core.go +1 -0
@@ -174,6 +174,7 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
174174 BehindTLSProxy: initialBehindTLSProxy, // Pass initial detection result
175175 AdminDomain: app.GetConfig().Http.FullDomain(),
176176 RootDomain: app.GetConfig().Http.Domain,
177+ HttpPort: config.DefaultHttpChallengePort,
177178 // RouteValidator: p.isHostInRoutes, // TODO: Pass a function if needed by hostPolicy
178179 }
179180
@@ -174,6 +174,7 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
174 BehindTLSProxy: initialBehindTLSProxy, // Pass initial detection result174 BehindTLSProxy: initialBehindTLSProxy, // Pass initial detection result
175 AdminDomain: app.GetConfig().Http.FullDomain(),175 AdminDomain: app.GetConfig().Http.FullDomain(),
176 RootDomain: app.GetConfig().Http.Domain,176 RootDomain: app.GetConfig().Http.Domain,
177+ HttpPort: config.DefaultHttpChallengePort,
177 // RouteValidator: p.isHostInRoutes, // TODO: Pass a function if needed by hostPolicy178 // RouteValidator: p.isHostInRoutes, // TODO: Pass a function if needed by hostPolicy
178 }179 }
179 180
M internal/proxy/server.go +12 -1
@@ -4,7 +4,6 @@ import (
44 "context"
55 "crypto/tls"
66 "fmt"
7- "net"
87 "net/http"
98 "strings"
109 "time"
@@ -145,6 +144,12 @@ func (p *Proxy) Start() error {
145144 }
146145 p.mu.Unlock()
147146
147+ // Explicitly request certificate for admin domain after ensuring route exists
148+ if err := p.requestDomainCertificate(adminDomain); err != nil {
149+ logger.Error("Failed to request initial certificate for admin domain", "domain", adminDomain, "error", err)
150+ // Don't return an error here, let the server start and retry later
151+ }
152+
148153 // Set up middleware
149154 p.setupMiddleware()
150155
@@ -301,6 +306,7 @@ func (p *Proxy) checkExternalPortAccess() {
301306 }
302307
303308 // Test HTTP port 80
309+ /* // Commenting out HTTP check due to potential hairpin NAT issues
304310 client := &http.Client{
305311 Timeout: 5 * time.Second,
306312 CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -316,8 +322,10 @@ func (p *Proxy) checkExternalPortAccess() {
316322 } else {
317323 logger.Info("External HTTP port 80 is accessible")
318324 }
325+ */
319326
320327 // Test HTTPS port 443
328+ /* // Commenting out httpsClient as it's no longer used
321329 httpsClient := &http.Client{
322330 Timeout: 10 * time.Second, // Increased timeout for more reliable testing
323331 Transport: &http.Transport{
@@ -339,6 +347,7 @@ func (p *Proxy) checkExternalPortAccess() {
339347 return http.ErrUseLastResponse
340348 },
341349 }
350+ */
342351
343352 // Verify TLS settings are consistent
344353 if p.certificateManager == nil { // Updated check
@@ -346,6 +355,7 @@ func (p *Proxy) checkExternalPortAccess() {
346355 "solution", "Check NewProxy implementation and logs")
347356 }
348357
358+ /* // Commenting out port 443 checks due to potential hairpin NAT issues
349359 // First try a basic TCP connection to port 443 to test if it's open
350360 logger.Info("Testing TCP connectivity to HTTPS port 443...")
351361 conn, tcpErr := net.DialTimeout("tcp", adminDomain+":443", 5*time.Second)
@@ -378,6 +388,7 @@ func (p *Proxy) checkExternalPortAccess() {
378388 } else {
379389 logger.Info("External HTTPS port 443 is accessible - TLS handshake successful")
380390 }
391+ */
381392 }
382393
383394 // // detectGordonContainer attempts to find the Gordon container automatically
@@ -4,7 +4,6 @@ import (
4 "context"4 "context"
5 "crypto/tls"5 "crypto/tls"
6 "fmt"6 "fmt"
7- "net"
8 "net/http"7 "net/http"
9 "strings"8 "strings"
10 "time"9 "time"
@@ -145,6 +144,12 @@ func (p *Proxy) Start() error {
145 }144 }
146 p.mu.Unlock()145 p.mu.Unlock()
147 146
147+ // Explicitly request certificate for admin domain after ensuring route exists
148+ if err := p.requestDomainCertificate(adminDomain); err != nil {
149+ logger.Error("Failed to request initial certificate for admin domain", "domain", adminDomain, "error", err)
150+ // Don't return an error here, let the server start and retry later
151+ }
152+
148 // Set up middleware153 // Set up middleware
149 p.setupMiddleware()154 p.setupMiddleware()
150 155
@@ -301,6 +306,7 @@ func (p *Proxy) checkExternalPortAccess() {
301 }306 }
302 307
303 // Test HTTP port 80308 // Test HTTP port 80
309+ /* // Commenting out HTTP check due to potential hairpin NAT issues
304 client := &http.Client{310 client := &http.Client{
305 Timeout: 5 * time.Second,311 Timeout: 5 * time.Second,
306 CheckRedirect: func(req *http.Request, via []*http.Request) error {312 CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -316,8 +322,10 @@ func (p *Proxy) checkExternalPortAccess() {
316 } else {322 } else {
317 logger.Info("External HTTP port 80 is accessible")323 logger.Info("External HTTP port 80 is accessible")
318 }324 }
325+ */
319 326
320 // Test HTTPS port 443327 // Test HTTPS port 443
328+ /* // Commenting out httpsClient as it's no longer used
321 httpsClient := &http.Client{329 httpsClient := &http.Client{
322 Timeout: 10 * time.Second, // Increased timeout for more reliable testing330 Timeout: 10 * time.Second, // Increased timeout for more reliable testing
323 Transport: &http.Transport{331 Transport: &http.Transport{
@@ -339,6 +347,7 @@ func (p *Proxy) checkExternalPortAccess() {
339 return http.ErrUseLastResponse347 return http.ErrUseLastResponse
340 },348 },
341 }349 }
350+ */
342 351
343 // Verify TLS settings are consistent352 // Verify TLS settings are consistent
344 if p.certificateManager == nil { // Updated check353 if p.certificateManager == nil { // Updated check
@@ -346,6 +355,7 @@ func (p *Proxy) checkExternalPortAccess() {
346 "solution", "Check NewProxy implementation and logs")355 "solution", "Check NewProxy implementation and logs")
347 }356 }
348 357
358+ /* // Commenting out port 443 checks due to potential hairpin NAT issues
349 // First try a basic TCP connection to port 443 to test if it's open359 // First try a basic TCP connection to port 443 to test if it's open
350 logger.Info("Testing TCP connectivity to HTTPS port 443...")360 logger.Info("Testing TCP connectivity to HTTPS port 443...")
351 conn, tcpErr := net.DialTimeout("tcp", adminDomain+":443", 5*time.Second)361 conn, tcpErr := net.DialTimeout("tcp", adminDomain+":443", 5*time.Second)
@@ -378,6 +388,7 @@ func (p *Proxy) checkExternalPortAccess() {
378 } else {388 } else {
379 logger.Info("External HTTPS port 443 is accessible - TLS handshake successful")389 logger.Info("External HTTPS port 443 is accessible - TLS handshake successful")
380 }390 }
391+ */
381 }392 }
382 393
383 // // detectGordonContainer attempts to find the Gordon container automatically394 // // detectGordonContainer attempts to find the Gordon container automatically
M internal/templating/models/templ/pages/login/index_templ.go +8 -8
@@ -65,23 +65,23 @@ func LoginPage(title string, buildVersion string, errorMsg string) templ.Compone
6565 if templ_7745c5c3_Err != nil {
6666 return templ_7745c5c3_Err
6767 }
68- if errorMsg == "empty_token" {
69- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Please enter a token.")
68+ if errorMsg == "server_error" {
69+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "A server error occurred during login. Please try again later.")
7070 if templ_7745c5c3_Err != nil {
7171 return templ_7745c5c3_Err
7272 }
73- } else if errorMsg == "invalid_token" {
74- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Invalid token. Please try again.")
73+ } else if errorMsg == "github_error" {
74+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "An error occurred during GitHub authentication. Please try again.")
7575 if templ_7745c5c3_Err != nil {
7676 return templ_7745c5c3_Err
7777 }
78- } else if errorMsg == "server_error" {
79- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "A server error occurred. Please try again later.")
78+ } else if errorMsg == "unauthorized" {
79+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "This GitHub account is not authorized.")
8080 if templ_7745c5c3_Err != nil {
8181 return templ_7745c5c3_Err
8282 }
8383 } else {
84- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "An error occurred. Please try again.")
84+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "An unknown login error occurred. Please try again.")
8585 if templ_7745c5c3_Err != nil {
8686 return templ_7745c5c3_Err
8787 }
@@ -91,7 +91,7 @@ func LoginPage(title string, buildVersion string, errorMsg string) templ.Compone
9191 return templ_7745c5c3_Err
9292 }
9393 }
94- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!-- Token Input Form --><div class=\"w-full\"><form action=\"login/submit-token\" method=\"POST\" class=\"mb-4\"><div class=\"form-control mb-4\"><label for=\"token\" class=\"label\"><span class=\"label-text\">Admin Token</span></label> <input type=\"text\" id=\"token\" name=\"token\" placeholder=\"Enter your admin token\" class=\"input input-bordered w-full\" required></div><button type=\"submit\" class=\"btn btn-primary w-full\">Continue with Token</button></form><div class=\"divider\">OR</div></div><!-- GitHub OAuth --><div class=\"w-full\"><a href=\"login/oauth/github\" class=\"btn btn-secondary w-full\"><svg class=\"mr-2 h-5 w-5\" fill=\"currentColor\" viewBox=\"0 0 20 20\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" d=\"M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z\" clip-rule=\"evenodd\"></path></svg> Sign in with GitHub</a></div></div></div><!-- End Login Section --></div></div></body></html>")
94+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!-- Token Input Form Removed --><!-- GitHub OAuth --><div class=\"w-full\"><a href=\"login/oauth/github\" class=\"btn btn-secondary w-full\"><svg class=\"mr-2 h-5 w-5\" fill=\"currentColor\" viewBox=\"0 0 20 20\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" d=\"M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z\" clip-rule=\"evenodd\"></path></svg> Sign in with GitHub</a></div></div></div><!-- End Login Section --></div></div></body></html>")
9595 if templ_7745c5c3_Err != nil {
9696 return templ_7745c5c3_Err
9797 }
@@ -65,23 +65,23 @@ func LoginPage(title string, buildVersion string, errorMsg string) templ.Compone
65 if templ_7745c5c3_Err != nil {65 if templ_7745c5c3_Err != nil {
66 return templ_7745c5c3_Err66 return templ_7745c5c3_Err
67 }67 }
68- if errorMsg == "empty_token" {68+ if errorMsg == "server_error" {
69- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Please enter a token.")69+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "A server error occurred during login. Please try again later.")
70 if templ_7745c5c3_Err != nil {70 if templ_7745c5c3_Err != nil {
71 return templ_7745c5c3_Err71 return templ_7745c5c3_Err
72 }72 }
73- } else if errorMsg == "invalid_token" {73+ } else if errorMsg == "github_error" {
74- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Invalid token. Please try again.")74+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "An error occurred during GitHub authentication. Please try again.")
75 if templ_7745c5c3_Err != nil {75 if templ_7745c5c3_Err != nil {
76 return templ_7745c5c3_Err76 return templ_7745c5c3_Err
77 }77 }
78- } else if errorMsg == "server_error" {78+ } else if errorMsg == "unauthorized" {
79- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "A server error occurred. Please try again later.")79+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "This GitHub account is not authorized.")
80 if templ_7745c5c3_Err != nil {80 if templ_7745c5c3_Err != nil {
81 return templ_7745c5c3_Err81 return templ_7745c5c3_Err
82 }82 }
83 } else {83 } else {
84- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "An error occurred. Please try again.")84+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "An unknown login error occurred. Please try again.")
85 if templ_7745c5c3_Err != nil {85 if templ_7745c5c3_Err != nil {
86 return templ_7745c5c3_Err86 return templ_7745c5c3_Err
87 }87 }
@@ -91,7 +91,7 @@ func LoginPage(title string, buildVersion string, errorMsg string) templ.Compone
91 return templ_7745c5c3_Err91 return templ_7745c5c3_Err
92 }92 }
93 }93 }
94- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!-- Token Input Form --><div class=\"w-full\"><form action=\"login/submit-token\" method=\"POST\" class=\"mb-4\"><div class=\"form-control mb-4\"><label for=\"token\" class=\"label\"><span class=\"label-text\">Admin Token</span></label> <input type=\"text\" id=\"token\" name=\"token\" placeholder=\"Enter your admin token\" class=\"input input-bordered w-full\" required></div><button type=\"submit\" class=\"btn btn-primary w-full\">Continue with Token</button></form><div class=\"divider\">OR</div></div><!-- GitHub OAuth --><div class=\"w-full\"><a href=\"login/oauth/github\" class=\"btn btn-secondary w-full\"><svg class=\"mr-2 h-5 w-5\" fill=\"currentColor\" viewBox=\"0 0 20 20\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" d=\"M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z\" clip-rule=\"evenodd\"></path></svg> Sign in with GitHub</a></div></div></div><!-- End Login Section --></div></div></body></html>")94+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!-- Token Input Form Removed --><!-- GitHub OAuth --><div class=\"w-full\"><a href=\"login/oauth/github\" class=\"btn btn-secondary w-full\"><svg class=\"mr-2 h-5 w-5\" fill=\"currentColor\" viewBox=\"0 0 20 20\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" d=\"M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z\" clip-rule=\"evenodd\"></path></svg> Sign in with GitHub</a></div></div></div><!-- End Login Section --></div></div></body></html>")
95 if templ_7745c5c3_Err != nil {95 if templ_7745c5c3_Err != nil {
96 return templ_7745c5c3_Err96 return templ_7745c5c3_Err
97 }97 }

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