// Package infra gère l'infrastructure (serveurs, containers, nginx). package infra import ( "context" "fmt" "path/filepath" "strings" ) const ( // NginxSitesAvailable est le répertoire des sites disponibles. NginxSitesAvailable = "/etc/nginx/sites-available" // NginxSitesEnabled est le répertoire des sites activés. NginxSitesEnabled = "/etc/nginx/sites-enabled" ) // NginxSiteInfo représente les infos d'un site Nginx. type NginxSiteInfo struct { Name string Enabled bool Config string HasSSL bool Domains []string Upstream string } // TestNginxConfig teste la configuration Nginx. func (c *SSHClient) TestNginxConfig(ctx context.Context) error { result, err := c.Exec(ctx, "nginx -t") if err != nil { return fmt.Errorf("nginx test: %w", err) } // nginx -t écrit sur stderr même en cas de succès if result.ExitCode != 0 { return fmt.Errorf("nginx config invalid: %s", result.Stderr) } return nil } // ReloadNginx recharge la configuration Nginx. func (c *SSHClient) ReloadNginx(ctx context.Context) error { // D'abord tester la config if err := c.TestNginxConfig(ctx); err != nil { return err } result, err := c.Exec(ctx, "systemctl reload nginx") if err != nil { return fmt.Errorf("nginx reload: %w", err) } if result.ExitCode != 0 { return fmt.Errorf("nginx reload failed: %s", result.Stderr) } return nil } // RestartNginx redémarre Nginx. func (c *SSHClient) RestartNginx(ctx context.Context) error { result, err := c.Exec(ctx, "systemctl restart nginx") if err != nil { return fmt.Errorf("nginx restart: %w", err) } if result.ExitCode != 0 { return fmt.Errorf("nginx restart failed: %s", result.Stderr) } return nil } // NginxStatus retourne le statut de Nginx. func (c *SSHClient) NginxStatus(ctx context.Context) (string, error) { result, err := c.Exec(ctx, "systemctl is-active nginx") if err != nil { return "unknown", nil } return strings.TrimSpace(result.Stdout), nil } // ListNginxSites liste les sites Nginx configurés. func (c *SSHClient) ListNginxSites(ctx context.Context) ([]NginxSiteInfo, error) { // Lister sites-available result, err := c.Exec(ctx, fmt.Sprintf("ls -1 %s", NginxSitesAvailable)) if err != nil { return nil, fmt.Errorf("list sites-available: %w", err) } if result.ExitCode != 0 { return nil, fmt.Errorf("list sites-available failed: %s", result.Stderr) } // Lister sites-enabled enabledResult, err := c.Exec(ctx, fmt.Sprintf("ls -1 %s", NginxSitesEnabled)) if err != nil { return nil, fmt.Errorf("list sites-enabled: %w", err) } enabledSet := make(map[string]bool) for _, name := range strings.Split(enabledResult.Stdout, "\n") { name = strings.TrimSpace(name) if name != "" { enabledSet[name] = true } } var sites []NginxSiteInfo for _, name := range strings.Split(result.Stdout, "\n") { name = strings.TrimSpace(name) if name == "" || name == "default" { continue } site := NginxSiteInfo{ Name: name, Enabled: enabledSet[name], } sites = append(sites, site) } return sites, nil } // GetNginxSiteConfig récupère la config d'un site. func (c *SSHClient) GetNginxSiteConfig(ctx context.Context, name string) (string, error) { path := filepath.Join(NginxSitesAvailable, name) content, err := c.ReadFile(ctx, path) if err != nil { return "", fmt.Errorf("read site config: %w", err) } return string(content), nil } // WriteNginxSiteConfig écrit la config d'un site. func (c *SSHClient) WriteNginxSiteConfig(ctx context.Context, name string, config string) error { path := filepath.Join(NginxSitesAvailable, name) return c.WriteFile(ctx, path, []byte(config), "644") } // EnableNginxSite active un site (crée le lien symbolique). func (c *SSHClient) EnableNginxSite(ctx context.Context, name string) error { src := filepath.Join(NginxSitesAvailable, name) dst := filepath.Join(NginxSitesEnabled, name) // Vérifier que le site existe exists, err := c.FileExists(ctx, src) if err != nil { return err } if !exists { return fmt.Errorf("site %s does not exist", name) } // Créer le lien result, err := c.Exec(ctx, fmt.Sprintf("ln -sf %s %s", src, dst)) if err != nil { return fmt.Errorf("enable site: %w", err) } if result.ExitCode != 0 { return fmt.Errorf("enable site failed: %s", result.Stderr) } return nil } // DisableNginxSite désactive un site (supprime le lien symbolique). func (c *SSHClient) DisableNginxSite(ctx context.Context, name string) error { path := filepath.Join(NginxSitesEnabled, name) result, err := c.Exec(ctx, fmt.Sprintf("rm -f %s", path)) if err != nil { return fmt.Errorf("disable site: %w", err) } if result.ExitCode != 0 { return fmt.Errorf("disable site failed: %s", result.Stderr) } return nil } // DeleteNginxSite supprime un site (désactive puis supprime). func (c *SSHClient) DeleteNginxSite(ctx context.Context, name string) error { // D'abord désactiver if err := c.DisableNginxSite(ctx, name); err != nil { return err } // Puis supprimer path := filepath.Join(NginxSitesAvailable, name) result, err := c.Exec(ctx, fmt.Sprintf("rm -f %s", path)) if err != nil { return fmt.Errorf("delete site: %w", err) } if result.ExitCode != 0 { return fmt.Errorf("delete site failed: %s", result.Stderr) } return nil } // DeployNginxSite déploie un site complet (écrire, activer, recharger). func (c *SSHClient) DeployNginxSite(ctx context.Context, name string, config string) error { // Écrire la config if err := c.WriteNginxSiteConfig(ctx, name, config); err != nil { return fmt.Errorf("write config: %w", err) } // Activer le site if err := c.EnableNginxSite(ctx, name); err != nil { return fmt.Errorf("enable site: %w", err) } // Recharger Nginx if err := c.ReloadNginx(ctx); err != nil { // En cas d'erreur, désactiver le site c.DisableNginxSite(ctx, name) return fmt.Errorf("reload nginx: %w", err) } return nil } // GenerateNginxProxyConfig génère une config proxy standard. func GenerateNginxProxyConfig(domain, upstream string, ssl bool) string { var config strings.Builder if ssl { // Redirect HTTP to HTTPS config.WriteString(fmt.Sprintf(`server { listen 80; listen [::]:80; server_name %s; return 301 https://$server_name$request_uri; } `, domain)) } // Main server block if ssl { config.WriteString(fmt.Sprintf(`server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name %s; ssl_certificate /etc/letsencrypt/live/%s/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/%s/privkey.pem; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; location / { proxy_pass %s; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; } } `, domain, domain, domain, upstream)) } else { config.WriteString(fmt.Sprintf(`server { listen 80; listen [::]:80; server_name %s; location / { proxy_pass %s; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; } } `, domain, upstream)) } return config.String() } // RequestSSLCertificate demande un certificat Let's Encrypt. func (c *SSHClient) RequestSSLCertificate(ctx context.Context, domain, email string) error { cmd := fmt.Sprintf("certbot certonly --nginx -d %s --non-interactive --agree-tos -m %s", domain, email) result, err := c.Exec(ctx, cmd) if err != nil { return fmt.Errorf("certbot: %w", err) } if result.ExitCode != 0 { return fmt.Errorf("certbot failed: %s", result.Stderr) } return nil } // CheckSSLCertificate vérifie si un certificat SSL existe. func (c *SSHClient) CheckSSLCertificate(ctx context.Context, domain string) (bool, error) { path := fmt.Sprintf("/etc/letsencrypt/live/%s/fullchain.pem", domain) return c.FileExists(ctx, path) }