feat: add persistence for last deployment path in Nginx handler
This commit is contained in:
@@ -56,6 +56,14 @@ pub trait FsHandler: Send + Sync + 'static {
|
|||||||
// clean up old config files that are applied to nginx
|
// clean up old config files that are applied to nginx
|
||||||
// keep only latest n deployments.
|
// keep only latest n deployments.
|
||||||
async fn cleanup_config(&self, n: usize) -> Result<()>;
|
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<Option<String>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FsHandlerImpl {
|
pub struct FsHandlerImpl {
|
||||||
@@ -91,6 +99,10 @@ impl FsHandlerImpl {
|
|||||||
self.get_deployment_dir().join(deployment_id)
|
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(
|
async fn get_deployment_config_path(
|
||||||
&self,
|
&self,
|
||||||
deployment_id: &str,
|
deployment_id: &str,
|
||||||
@@ -183,6 +195,58 @@ impl FsHandler for FsHandlerImpl {
|
|||||||
Ok(full_output_path.to_string_lossy().to_string())
|
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<Option<String>> {
|
||||||
|
// 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<()> {
|
async fn cleanup_config(&self, n: usize) -> Result<()> {
|
||||||
let deployment_dir = self.get_deployment_dir();
|
let deployment_dir = self.get_deployment_dir();
|
||||||
// loop through all files in the deployment dir and delete them
|
// loop through all files in the deployment dir and delete them
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use nxmesh_proto::{
|
|||||||
agent_message::Payload::ConfigUpdateResult as ConfigUpdateResultPayload, command::Command,
|
agent_message::Payload::ConfigUpdateResult as ConfigUpdateResultPayload, command::Command,
|
||||||
command_result,
|
command_result,
|
||||||
};
|
};
|
||||||
use tracing::warn;
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::settings::NginxSettings,
|
config::settings::NginxSettings,
|
||||||
@@ -106,6 +106,9 @@ impl OnConfigUpdateHandler for NginxMasterMessageHandlerImpl {
|
|||||||
}
|
}
|
||||||
// apply reload on the root config
|
// apply reload on the root config
|
||||||
self.command_handler.reload(Some(&root_config_path)).await?;
|
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
|
// Reply the master to confirm the config update is successful
|
||||||
self.master_handler
|
self.master_handler
|
||||||
.send_message_to_master(nxmesh_proto::AgentMessage {
|
.send_message_to_master(nxmesh_proto::AgentMessage {
|
||||||
@@ -141,17 +144,25 @@ impl OnCommandHandler for NginxMasterMessageHandlerImpl {
|
|||||||
message_id: message_id.to_string(),
|
message_id: message_id.to_string(),
|
||||||
payload: None,
|
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 {
|
let result: command_result::Result = match command {
|
||||||
// TODO: should use the previous config path
|
|
||||||
Command::Reload(_) => {
|
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 {
|
command_result::Result::ReloadResult(nxmesh_proto::ReloadResult {
|
||||||
success: result.is_ok(),
|
success: result.is_ok(),
|
||||||
error_message: result.err().map(|e| e.to_string()).unwrap_or_default(),
|
error_message: result.err().map(|e| e.to_string()).unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Command::Test(_) => {
|
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 {
|
command_result::Result::TestResult(nxmesh_proto::TestResult {
|
||||||
success: result.is_ok(),
|
success: result.is_ok(),
|
||||||
error_message: result.err().map(|e| e.to_string()).unwrap_or_default(),
|
error_message: result.err().map(|e| e.to_string()).unwrap_or_default(),
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ message ConfigContent {
|
|||||||
|
|
||||||
message ConfigUpdateResult {
|
message ConfigUpdateResult {
|
||||||
string config_id = 1; // should match the config_id in ConfigUpdate
|
string config_id = 1; // should match the config_id in ConfigUpdate
|
||||||
bool success = 2;
|
string version = 2;
|
||||||
ConfigUpdateError error_message = 3; // if success is false, this field should contain the error message
|
bool success = 3;
|
||||||
|
optional ConfigUpdateError error_message = 4; // if success is false, this field should contain the error message
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ConfigUpdateError {
|
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"
|
// Command represents a request from master to agent to execute a command, e.g. "reload", "test"
|
||||||
message Command {
|
message Command {
|
||||||
oneof command {
|
oneof command {
|
||||||
|
|||||||
Reference in New Issue
Block a user