use tracing::{info, warn}; use crate::commands::{run::run_cmd, write_config::INTERNAL_CONFIG_FOLDER_NAME}; use std::path::PathBuf; pub struct ValidateCommand { nginx_config_dir: PathBuf, } impl ValidateCommand { pub fn new(nginx_config_dir: PathBuf) -> Self { Self { nginx_config_dir } } pub fn nginx_config_dir(&self) -> PathBuf { self.nginx_config_dir.clone() } pub async fn validate_all( &self, ) -> Result<(i32, String), Box> { // Try a normal config test first. If it fails due to pid permission // errors (common when running unprivileged against /run/nginx.pid), // retry with a writable pid override so validation can succeed. match run_cmd("nginx", &["-t"], 10).await { Ok(res) => Ok(res), Err(e) => { info!( "nginx -t failed: {}. Trying with privileged wrapper or writable pid override.", e ); let es = e.to_string(); if es.contains("/run/nginx.pid") && es.contains("Permission denied") { // Try privileged validate wrapper if available (allows the agent to run // nginx -t via sudo without modifying the main config). match run_cmd( "sudo", // TODO: allow configuring the path to the wrapper &["-n", "/usr/local/sbin/yanpm-nginx-validate"], 10, ) .await { Ok(res) => return Ok(res), Err(e) => { warn!( "Privileged validate wrapper failed: {}. Falling back to writable pid override.", e ); // Fallback to the existing writable-pid override if sudo wrapper // isn't available or fails. let pid_path = format!( "{}/yanpm-validate-{}.pid", std::env::temp_dir().display(), std::process::id() ); let g_arg = format!("pid {};", pid_path); let args_vec = ["-t".to_string(), "-g".to_string(), g_arg]; let args_ref: Vec<&str> = args_vec.iter().map(|s| s.as_str()).collect(); return run_cmd("nginx", args_ref.as_slice(), 10).await; } } } Err(e) } } } pub async fn validate( &self, config_name: &str, timestamp: u64, ) -> Result<(i32, String), Box> { let filename = crate::commands::run::to_file_name(config_name, timestamp)?; // fragments are written into the YANPM subdirectory let full_path = self .nginx_config_dir .join(INTERNAL_CONFIG_FOLDER_NAME) .join(&filename); // ensure the fragment file exists if tokio::fs::metadata(&full_path).await.is_err() { return Err(format!("Config file not found: {}", full_path.display()).into()); } // Create a temporary wrapper nginx config that provides the required // top-level sections (`events` and `http`) and includes the fragment. let fragment_path = full_path.to_str().ok_or("invalid config path")?.to_string(); let mut tmp_path = std::env::temp_dir(); let tmp_name = format!("yanpm-validate-{}-{}.conf", timestamp, std::process::id()); tmp_path.push(tmp_name); let wrapper = format!( "worker_processes 1;\nevents {{ worker_connections 1024; }}\nhttp {{\n include {};\n}}\n", fragment_path ); // Write the temporary wrapper file tokio::fs::write(&tmp_path, wrapper).await?; let tmp_path_str = tmp_path .to_str() .ok_or("invalid temp config path")? .to_string(); // Run the test against the wrapper, telling nginx to place its pid // somewhere writable so the config test doesn't fail with permission // errors when running as an unprivileged user. let result = match run_cmd("nginx", &["-t", "-c", &tmp_path_str], 10).await { Ok(res) => Ok(res), Err(e) => { info!( "nginx -t failed: {}. Trying with privileged wrapper or writable pid override.", e ); let es = e.to_string(); if es.contains("/run/nginx.pid") && es.contains("Permission denied") { // Try privileged validate wrapper if available (allows the agent to run // nginx -t via sudo without modifying the main config). match run_cmd( "sudo", // TODO: allow configuring the path to the wrapper &[ "-n", "/usr/local/sbin/yanpm-nginx-validate-file", &tmp_path_str, ], 10, ) .await { Ok(res) => return Ok(res), Err(e) => { warn!( "Privileged validate wrapper failed: {}. Falling back to writable pid override.", e ); let pid_path = format!( "{}/yanpm-validate-{}.pid", std::env::temp_dir().display(), std::process::id() ); let g_arg = format!("pid {};", pid_path); let args_vec = [ "-t".to_string(), "-c".to_string(), tmp_path_str.clone(), "-g".to_string(), g_arg, ]; let args_ref: Vec<&str> = args_vec.iter().map(|s| s.as_str()).collect(); return run_cmd("nginx", args_ref.as_slice(), 10).await; } } } Err(e) } }; let _ = tokio::fs::remove_file(&tmp_path).await; result } }