diff --git a/apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs b/apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs index 053b17b..b9ca2f2 100644 --- a/apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs +++ b/apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs @@ -56,6 +56,14 @@ pub trait FsHandler: Send + Sync + 'static { // clean up old config files that are applied to nginx // keep only latest n deployments. async fn cleanup_config(&self, n: usize) -> Result<()>; + + // Persist the root config path of the last successful deployment. + // Survives agent restarts so Reload/Test commands work without a new ConfigUpdate. + async fn save_last_deployment(&self, root_config_path: &str) -> Result<()>; + + // Load the last persisted root config path, if any. + // Returns Ok(None) when no state file exists or it is empty/corrupt. + async fn load_last_deployment(&self) -> Result>; } pub struct FsHandlerImpl { @@ -91,6 +99,10 @@ impl FsHandlerImpl { self.get_deployment_dir().join(deployment_id) } + fn get_state_file_path(&self) -> std::path::PathBuf { + std::path::Path::new(&self.settings.nginx_config_path).join(".last_deployment") + } + async fn get_deployment_config_path( &self, deployment_id: &str, @@ -183,6 +195,58 @@ impl FsHandler for FsHandlerImpl { Ok(full_output_path.to_string_lossy().to_string()) } + async fn save_last_deployment(&self, root_config_path: &str) -> Result<()> { + let state_path = self.get_state_file_path(); + let tmp_path = state_path.with_extension("tmp"); + tokio::fs::write(&tmp_path, format!("{}\n", root_config_path)).await?; + tokio::fs::rename(&tmp_path, &state_path).await?; + Ok(()) + } + + async fn load_last_deployment(&self) -> Result> { + // primary: try state file + let state_path = self.get_state_file_path(); + if state_path.exists() { + let content = tokio::fs::read_to_string(&state_path).await?; + let path = content.trim().to_string(); + if !path.is_empty() { + return Ok(Some(path)); + } + } + + // fallback: scan deployments directory for the newest deployment + let deployment_dir = self.get_deployment_dir(); + if !deployment_dir.exists() { + return Ok(None); + } + let mut entries = tokio::fs::read_dir(&deployment_dir).await?; + let mut candidates: Vec<(std::path::PathBuf, std::time::SystemTime)> = Vec::new(); + while let Some(entry) = entries.next_entry().await? { + if entry.file_type().await.map_or(false, |t| t.is_dir()) { + if let Ok(mtime) = entry.metadata().await.and_then(|m| m.modified()) { + candidates.push((entry.path(), mtime)); + } + } + } + // sort descending by mtime (newest first) + candidates.sort_by(|a, b| b.1.cmp(&a.1)); + + for (dir, _) in &candidates { + let mut dir_entries = tokio::fs::read_dir(dir).await?; + while let Some(file) = dir_entries.next_entry().await? { + if file.file_type().await.map_or(false, |t| t.is_file()) { + let name = file.file_name().to_string_lossy().to_string(); + if name == "nginx.conf" || name.ends_with(".conf") { + let path = file.path().to_string_lossy().to_string(); + return Ok(Some(path)); + } + } + } + } + + Ok(None) + } + async fn cleanup_config(&self, n: usize) -> Result<()> { let deployment_dir = self.get_deployment_dir(); // loop through all files in the deployment dir and delete them diff --git a/apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs b/apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs index a0a994f..51b750a 100644 --- a/apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs +++ b/apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs @@ -6,7 +6,7 @@ use nxmesh_proto::{ agent_message::Payload::ConfigUpdateResult as ConfigUpdateResultPayload, command::Command, command_result, }; -use tracing::warn; +use tracing::{info, warn}; use crate::{ config::settings::NginxSettings, @@ -106,6 +106,9 @@ impl OnConfigUpdateHandler for NginxMasterMessageHandlerImpl { } // apply reload on the root config self.command_handler.reload(Some(&root_config_path)).await?; + // persist deployment path so Reload/Test commands survive agent restarts + self.fs_handler.save_last_deployment(&root_config_path).await?; + info!("Persisted last deployment path: {}", root_config_path); // Reply the master to confirm the config update is successful self.master_handler .send_message_to_master(nxmesh_proto::AgentMessage { @@ -141,17 +144,25 @@ impl OnCommandHandler for NginxMasterMessageHandlerImpl { message_id: message_id.to_string(), payload: None, }; + // load the last known deployment path for use with Reload/Test commands + let last_config_path = self.fs_handler.load_last_deployment().await?; + let result: command_result::Result = match command { - // TODO: should use the previous config path Command::Reload(_) => { - let result = self.command_handler.reload(None).await; + let result = self + .command_handler + .reload(last_config_path.as_deref()) + .await; command_result::Result::ReloadResult(nxmesh_proto::ReloadResult { success: result.is_ok(), error_message: result.err().map(|e| e.to_string()).unwrap_or_default(), }) } Command::Test(_) => { - let result = self.command_handler.validate(None).await; + let result = self + .command_handler + .validate(last_config_path.as_deref()) + .await; command_result::Result::TestResult(nxmesh_proto::TestResult { success: result.is_ok(), error_message: result.err().map(|e| e.to_string()).unwrap_or_default(), diff --git a/crates/nxmesh-proto/proto/agent.proto b/crates/nxmesh-proto/proto/agent.proto index 9f57edd..b63e606 100644 --- a/crates/nxmesh-proto/proto/agent.proto +++ b/crates/nxmesh-proto/proto/agent.proto @@ -68,8 +68,9 @@ message ConfigContent { message ConfigUpdateResult { string config_id = 1; // should match the config_id in ConfigUpdate - bool success = 2; - ConfigUpdateError error_message = 3; // if success is false, this field should contain the error message + string version = 2; + bool success = 3; + optional ConfigUpdateError error_message = 4; // if success is false, this field should contain the error message } enum ConfigUpdateError { @@ -83,6 +84,8 @@ enum ConfigUpdateError { // // +// TODO: allow setting the default fallback and the corresponding default nginx root config when nginx reload fails to re-use old config, "use default config", "stop nginx". + // Command represents a request from master to agent to execute a command, e.g. "reload", "test" message Command { oneof command {