feat: add nginx reload and validation wrappers with sudo permissions
This commit is contained in:
@@ -93,6 +93,17 @@ impl ReloadCommand {
|
||||
}
|
||||
|
||||
// reload the running nginx master process (no -c) so it reloads its configured main config
|
||||
run_cmd("nginx", &["-s", "reload"], 10).await
|
||||
// Prefer the restricted sudo wrapper if available, fall back to direct nginx reload.
|
||||
// TODO: allow configuring the path to the wrapper
|
||||
match run_cmd("sudo", &["-n", "/usr/local/sbin/yanpm-nginx-reload"], 10).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => {
|
||||
error!(
|
||||
"sudo reload wrapper failed, falling back to direct nginx reload: {}",
|
||||
e
|
||||
);
|
||||
run_cmd("nginx", &["-s", "reload"], 10).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::commands::{run::run_cmd, write_config::INTERNAL_CONFIG_FOLDER_NAME};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -17,7 +19,51 @@ impl ValidateCommand {
|
||||
pub async fn validate_all(
|
||||
&self,
|
||||
) -> Result<(i32, String), Box<dyn std::error::Error + Send + Sync>> {
|
||||
run_cmd("nginx", &["-t"], 10).await
|
||||
// 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(
|
||||
@@ -37,11 +83,84 @@ impl ValidateCommand {
|
||||
return Err(format!("Config file not found: {}", full_path.display()).into());
|
||||
}
|
||||
|
||||
run_cmd(
|
||||
"nginx",
|
||||
&["-t", "-c", full_path.to_str().ok_or("invalid config path")?],
|
||||
10,
|
||||
)
|
||||
.await
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::info;
|
||||
|
||||
use crate::commands::run::to_file_name;
|
||||
|
||||
@@ -26,6 +27,7 @@ impl WriteConfigCommand {
|
||||
let path = self.nginx_config_dir.clone();
|
||||
// ensure main config dir exists
|
||||
tokio::fs::create_dir_all(&path).await?;
|
||||
info!("Writing config to {:?}", path.join(&filename));
|
||||
|
||||
// create YANPM subdir where fragment files live
|
||||
let yanpm_dir = path.join(INTERNAL_CONFIG_FOLDER_NAME);
|
||||
@@ -62,7 +64,7 @@ impl WriteConfigCommand {
|
||||
|
||||
// set explicit permissions (rw-r-----)
|
||||
tokio::fs::set_permissions(&final_path, std::fs::Permissions::from_mode(0o640)).await?;
|
||||
|
||||
info!("Config written and permissions set for {:?}", final_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub async fn validate(
|
||||
}
|
||||
};
|
||||
|
||||
let (_code, _output) = match nginx_controller
|
||||
let resp = match nginx_controller
|
||||
.validate(¶ms.config_name, params.timestamp)
|
||||
.await
|
||||
{
|
||||
@@ -54,7 +54,7 @@ pub async fn validate(
|
||||
}
|
||||
};
|
||||
|
||||
(axum::http::StatusCode::OK,).into_response()
|
||||
(axum::http::StatusCode::OK, axum::Json(resp)).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user