Files
YANPM/apps/agent/src/commands/reload.rs

110 lines
4.1 KiB
Rust

use std::path::Path;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex;
use tracing::error;
use crate::commands::write_config::INTERNAL_CONFIG_FOLDER_NAME;
use crate::commands::{run::run_cmd, validate::ValidateCommand};
pub struct ReloadCommand {
is_reloading: Mutex<bool>,
}
struct ReloadResetGuard<'a> {
guard: tokio::sync::MutexGuard<'a, bool>,
}
impl<'a> Drop for ReloadResetGuard<'a> {
fn drop(&mut self) {
*self.guard = false;
}
}
impl Default for ReloadCommand {
fn default() -> Self {
Self {
is_reloading: Mutex::new(false),
}
}
}
impl ReloadCommand {
pub async fn validate_and_reload(
&self,
config_name: &str,
timestamp: u64,
validate_cmd: Arc<ValidateCommand>,
) -> Result<(i32, String), Box<dyn std::error::Error + Send + Sync>> {
// ensure the written fragment exists
validate_cmd.validate(config_name, timestamp).await?;
// Now atomically swap the YANPM.conf symlink to point to the new fragment
// so nginx -t validates the composed main config. If validation fails,
// attempt to restore the previous symlink.
let filename = crate::commands::run::to_file_name(config_name, timestamp)?;
let nginx_dir = validate_cmd.nginx_config_dir();
let symlink_path = nginx_dir.join("YANPM.conf");
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
let tmp_name = format!("YANPM.conf.tmp.{}.{}", std::process::id(), now);
let tmp_path = nginx_dir.join(&tmp_name);
// prepare relative target: INTERNAL_CONFIG_FOLDER_NAME/<filename>
let rel_target = Path::new(INTERNAL_CONFIG_FOLDER_NAME).join(&filename);
// read previous target if exists
let previous_target = std::fs::read_link(&symlink_path).ok();
// Acquire reload guard before mutating the symlink to avoid races
let reloading_lock = self.is_reloading.lock().await;
if *reloading_lock {
return Err("Reload already in progress".into());
}
// set flag to true and ensure it is reset on drop
let mut mut_guard = reloading_lock;
*mut_guard = true;
let _reset_guard = ReloadResetGuard { guard: mut_guard };
// create temporary symlink and atomically rename into place
std::os::unix::fs::symlink(&rel_target, &tmp_path)?;
tokio::fs::rename(&tmp_path, &symlink_path).await?;
// validate composed main config now that symlink points to new fragment
if let Err(e) = validate_cmd.validate_all().await {
// restore previous symlink state while still holding the guard
if let Some(prev) = previous_target {
let restore_tmp =
nginx_dir.join(format!("YANPM.conf.restore.{}.{}", std::process::id(), now));
std::os::unix::fs::symlink(&prev, &restore_tmp)?;
if let Err(err) = tokio::fs::rename(&restore_tmp, &symlink_path).await {
error!(
"Failed to restore previous YANPM.conf symlink after validation error: {}",
err
);
}
} else if let Err(err) = tokio::fs::remove_file(&symlink_path).await {
error!(
"Failed to remove YANPM.conf symlink after validation error: {}",
err
);
}
return Err(e);
}
// reload the running nginx master process (no -c) so it reloads its configured main config
// 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
}
}
}
}