From d64d27e7993d51be986639179f603cdc85a1aa84 Mon Sep 17 00:00:00 2001 From: Zachariah Ngonyani Date: Wed, 20 Aug 2025 19:55:37 +0300 Subject: [PATCH 1/2] fix: resolve nginx first-run installation failures - Enhanced path detection for Apple Silicon vs Intel homebrew installations - Added comprehensive log file permission handling with proper ownership - Implemented PID file cleanup to prevent root-owned file conflicts - Added pre-start PID file validation and removal in service startup - Improved nginx service start verification with status checking - Fixed cross-platform directory and file permission management Resolves issue where nginx installation would fail on first run due to permission conflicts with log files and PID files created by previous root processes. Installation now succeeds reliably on first attempt. --- internal/installer/nginx.go | 159 +++++++++++++++++++++++++++++++++--- 1 file changed, 146 insertions(+), 13 deletions(-) diff --git a/internal/installer/nginx.go b/internal/installer/nginx.go index 2ade395..556511b 100644 --- a/internal/installer/nginx.go +++ b/internal/installer/nginx.go @@ -8,6 +8,8 @@ import ( "path/filepath" "runtime" "strings" + "syscall" + "time" ) // NginxInstaller handles nginx configuration for Vertex @@ -30,15 +32,21 @@ func NewNginxInstaller(domain, port string) *NginxInstaller { // Set platform-specific paths switch runtime.GOOS { case "darwin": - // Detect homebrew installation path - if _, err := os.Stat("/opt/homebrew/etc/nginx"); err == nil { - // Apple Silicon homebrew path + // Detect homebrew installation path by checking where nginx is installed + if nginxPath, err := exec.LookPath("nginx"); err == nil { + if strings.Contains(nginxPath, "/opt/homebrew/") { + // Apple Silicon homebrew path + ni.ConfigPath = "/opt/homebrew/etc/nginx/nginx.conf" + ni.SitesPath = "/opt/homebrew/etc/nginx/servers" + } else { + // Intel homebrew path + ni.ConfigPath = "/usr/local/etc/nginx/nginx.conf" + ni.SitesPath = "/usr/local/etc/nginx/servers" + } + } else { + // Default to Apple Silicon if nginx not found yet (will be installed) ni.ConfigPath = "/opt/homebrew/etc/nginx/nginx.conf" ni.SitesPath = "/opt/homebrew/etc/nginx/servers" - } else { - // Intel homebrew path - ni.ConfigPath = "/usr/local/etc/nginx/nginx.conf" - ni.SitesPath = "/usr/local/etc/nginx/servers" } case "linux": // Standard Linux nginx paths @@ -205,6 +213,8 @@ func (ni *NginxInstaller) createSitesDirectory() error { // createNginxDirectories creates nginx log and run directories with proper permissions func (ni *NginxInstaller) createNginxDirectories() error { var directories []string + var logFiles []string + var pidFiles []string switch runtime.GOOS { case "darwin": @@ -214,17 +224,38 @@ func (ni *NginxInstaller) createNginxDirectories() error { "/opt/homebrew/var/log/nginx", "/opt/homebrew/var/run", } + logFiles = []string{ + "/opt/homebrew/var/log/nginx/access.log", + "/opt/homebrew/var/log/nginx/error.log", + } + pidFiles = []string{ + "/opt/homebrew/var/run/nginx.pid", + } } else { directories = []string{ "/usr/local/var/log/nginx", "/usr/local/var/run", } + logFiles = []string{ + "/usr/local/var/log/nginx/access.log", + "/usr/local/var/log/nginx/error.log", + } + pidFiles = []string{ + "/usr/local/var/run/nginx.pid", + } } case "linux": directories = []string{ "/var/log/nginx", "/var/run/nginx", } + logFiles = []string{ + "/var/log/nginx/access.log", + "/var/log/nginx/error.log", + } + pidFiles = []string{ + "/var/run/nginx/nginx.pid", + } default: // Skip directory creation for unsupported platforms return nil @@ -270,6 +301,74 @@ func (ni *NginxInstaller) createNginxDirectories() error { } } + // Create and fix log files with proper ownership + for _, logFile := range logFiles { + // Create log file if it doesn't exist + if _, err := os.Stat(logFile); os.IsNotExist(err) { + // Create empty log file + cmd := exec.Command("sudo", "touch", logFile) + if err := cmd.Run(); err != nil { + fmt.Printf("⚠️ Could not create log file %s: %v\n", logFile, err) + continue + } + } + + // Fix ownership of existing log files + if runtime.GOOS == "darwin" { + currentUser := os.Getenv("USER") + if currentUser != "" { + cmd := exec.Command("sudo", "chown", currentUser+":admin", logFile) + if err := cmd.Run(); err != nil { + fmt.Printf("⚠️ Could not fix ownership of %s: %v\n", logFile, err) + } else { + fmt.Printf("✅ Fixed ownership of %s\n", logFile) + } + } + } + + // Set proper permissions on log file + cmd := exec.Command("sudo", "chmod", "644", logFile) + if err := cmd.Run(); err != nil { + fmt.Printf("⚠️ Could not set permissions on %s: %v\n", logFile, err) + } + } + + // Clean up and fix PID files and directories + for _, pidFile := range pidFiles { + // Ensure the parent directory has correct ownership first + pidDir := filepath.Dir(pidFile) + if runtime.GOOS == "darwin" { + currentUser := os.Getenv("USER") + if currentUser != "" { + cmd := exec.Command("sudo", "chown", currentUser+":admin", pidDir) + if err := cmd.Run(); err != nil { + fmt.Printf("⚠️ Could not fix ownership of %s: %v\n", pidDir, err) + } else { + fmt.Printf("✅ Fixed ownership of %s\n", pidDir) + } + } + } + + // Set proper permissions on PID directory + cmd := exec.Command("sudo", "chmod", "755", pidDir) + if err := cmd.Run(); err != nil { + fmt.Printf("⚠️ Could not set permissions on %s: %v\n", pidDir, err) + } + + // Remove existing PID file if it exists and is owned by root + if stat, err := os.Stat(pidFile); err == nil { + // Check if file is owned by root (UID 0) + if sys := stat.Sys(); sys != nil { + if stat, ok := sys.(*syscall.Stat_t); ok && stat.Uid == 0 { + cmd := exec.Command("sudo", "rm", "-f", pidFile) + if err := cmd.Run(); err == nil { + fmt.Printf("✅ Removed root-owned PID file %s\n", pidFile) + } + } + } + } + } + fmt.Printf("✅ Nginx directories created\n") return nil } @@ -591,13 +690,47 @@ func (ni *NginxInstaller) testNginxConfig() error { func (ni *NginxInstaller) startNginxService() error { switch runtime.GOOS { case "darwin": - // Use brew services on macOS - cmd := exec.Command("brew", "services", "restart", "nginx") - if err := cmd.Run(); err != nil { - return err + // Stop nginx first to ensure clean restart + stopCmd := exec.Command("brew", "services", "stop", "nginx") + stopCmd.Run() // Ignore errors if service wasn't running + + // Clean up any root-owned PID files before starting + pidFile := "/opt/homebrew/var/run/nginx.pid" + if strings.Contains(ni.ConfigPath, "/usr/local/") { + pidFile = "/usr/local/var/run/nginx.pid" } - fmt.Printf("✅ Nginx service started\n") - return nil + + if stat, err := os.Stat(pidFile); err == nil { + if sys := stat.Sys(); sys != nil { + if stat, ok := sys.(*syscall.Stat_t); ok && stat.Uid == 0 { + cmd := exec.Command("sudo", "rm", "-f", pidFile) + if err := cmd.Run(); err == nil { + fmt.Printf("✅ Cleaned root-owned PID file before start\n") + } + } + } + } + + // Start nginx service + cmd := exec.Command("brew", "services", "start", "nginx") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to start nginx: %s", string(output)) + } + + // Wait a moment for service to fully start + time.Sleep(2 * time.Second) + + // Verify nginx is actually running by checking the service status + statusCmd := exec.Command("brew", "services", "list") + statusOutput, err := statusCmd.Output() + if err == nil && strings.Contains(string(statusOutput), "nginx") && strings.Contains(string(statusOutput), "started") { + fmt.Printf("✅ Nginx service started\n") + return nil + } + + return fmt.Errorf("nginx service appears to have failed to start properly") + case "linux": // Use systemctl on Linux cmd := exec.Command("sudo", "systemctl", "enable", "nginx") From 6565f7d2ac8c37d663b9495615b65ebaea287fab Mon Sep 17 00:00:00 2001 From: Zachariah Ngonyani Date: Wed, 20 Aug 2025 20:05:37 +0300 Subject: [PATCH 2/2] fix: replace syscall.Stat_t with cross-platform file ownership check - Remove platform-specific syscall import that fails on Linux CI/CD - Replace syscall.Stat_t usage with portable 'ls -la' command parsing - Maintain same functionality for detecting root-owned PID files - Ensures compatibility across macOS, Linux, and CI/CD environments Resolves build failures in CI/CD pipelines while preserving the nginx permission fix functionality for first-run installations. --- internal/installer/nginx.go | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/internal/installer/nginx.go b/internal/installer/nginx.go index 556511b..21ad481 100644 --- a/internal/installer/nginx.go +++ b/internal/installer/nginx.go @@ -8,7 +8,6 @@ import ( "path/filepath" "runtime" "strings" - "syscall" "time" ) @@ -356,14 +355,15 @@ func (ni *NginxInstaller) createNginxDirectories() error { } // Remove existing PID file if it exists and is owned by root - if stat, err := os.Stat(pidFile); err == nil { - // Check if file is owned by root (UID 0) - if sys := stat.Sys(); sys != nil { - if stat, ok := sys.(*syscall.Stat_t); ok && stat.Uid == 0 { - cmd := exec.Command("sudo", "rm", "-f", pidFile) - if err := cmd.Run(); err == nil { - fmt.Printf("✅ Removed root-owned PID file %s\n", pidFile) - } + if _, err := os.Stat(pidFile); err == nil { + // Check if file is owned by root using ls command + cmd := exec.Command("ls", "-la", pidFile) + output, err := cmd.Output() + if err == nil && strings.Contains(string(output), " root ") { + // File is owned by root, remove it + cmd := exec.Command("sudo", "rm", "-f", pidFile) + if err := cmd.Run(); err == nil { + fmt.Printf("✅ Removed root-owned PID file %s\n", pidFile) } } } @@ -700,13 +700,15 @@ func (ni *NginxInstaller) startNginxService() error { pidFile = "/usr/local/var/run/nginx.pid" } - if stat, err := os.Stat(pidFile); err == nil { - if sys := stat.Sys(); sys != nil { - if stat, ok := sys.(*syscall.Stat_t); ok && stat.Uid == 0 { - cmd := exec.Command("sudo", "rm", "-f", pidFile) - if err := cmd.Run(); err == nil { - fmt.Printf("✅ Cleaned root-owned PID file before start\n") - } + if _, err := os.Stat(pidFile); err == nil { + // Check if file is owned by root using ls command + cmd := exec.Command("ls", "-la", pidFile) + output, err := cmd.Output() + if err == nil && strings.Contains(string(output), " root ") { + // File is owned by root, remove it + cmd := exec.Command("sudo", "rm", "-f", pidFile) + if err := cmd.Run(); err == nil { + fmt.Printf("✅ Cleaned root-owned PID file before start\n") } } }