feat: added config update message handling

This commit is contained in:
GW_MC
2026-06-06 10:45:01 +00:00
parent f7162f0c17
commit c0d243f661
12 changed files with 345 additions and 81 deletions

View File

@@ -1,19 +1,42 @@
use std::sync::Arc;
use anyhow::{Context, Result};
use fs4::tokio::AsyncFileExt;
use thiserror::Error;
use tokio::{io::AsyncWriteExt, process::Command};
use tracing::{debug, warn};
use tracing::warn;
use crate::config::settings::NginxSettings;
use crate::{config::settings::NginxSettings, service::master_handler::MasterHandlerError};
#[cfg(test)]
use mockall::predicate::*;
// TODO: custom error type
#[derive(Debug, Error)]
pub enum FsHandlerError {
#[error("Invalid output path: {0}")]
InvalidOutputPath(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
impl From<FsHandlerError> for MasterHandlerError {
fn from(err: FsHandlerError) -> Self {
MasterHandlerError::MessageHandlingError(format!("File system handling error: {}", err))
}
}
pub type FsResult<T> = std::result::Result<T, FsHandlerError>;
type Result<T> = FsResult<T>;
#[async_trait::async_trait]
#[cfg_attr(test, mockall::automock)]
pub trait FsHandler: Send + Sync + 'static {
fn get_deployment_id(config_id: &str, version: &str) -> String
where
Self: Sized,
{
format!("{}-{}", config_id, version)
}
// Write a new config file for nginx.
// The output_path is a relative path to the nginx config directory of the deployment folder. The actual path to the config should not be assumed by the caller, as it can be different in different environments, but will be promised to be relative to the deployment folder for each the corresponding deployment_id. Path traversal is not allowed.
async fn write_config(
@@ -46,10 +69,16 @@ impl FsHandlerImpl {
fn validate_config_path(config_path: &str) -> Result<()> {
if !std::path::Path::new(config_path).exists() {
anyhow::bail!("Config file not found at path: {}", config_path);
return Err(FsHandlerError::InvalidOutputPath(format!(
"Config file not found at path: {}",
config_path
)));
}
if !std::path::Path::new(config_path).is_file() {
anyhow::bail!("Config path is not a file: {}", config_path);
return Err(FsHandlerError::InvalidOutputPath(format!(
"Config path is not a file: {}",
config_path
)));
}
Ok(())
}
@@ -70,13 +99,17 @@ impl FsHandlerImpl {
) -> Result<std::path::PathBuf> {
let output_path_obj = std::path::Path::new(output_path);
if output_path_obj.is_absolute() {
anyhow::bail!("Output path must be a relative path");
return Err(FsHandlerError::InvalidOutputPath(
"Output path must be a relative path".into(),
));
}
if output_path_obj
.components()
.any(|comp| comp == std::path::Component::ParentDir)
{
anyhow::bail!("Output path must not contain parent directory traversal");
return Err(FsHandlerError::InvalidOutputPath(
"Output path must not contain parent directory traversal".into(),
));
}
let deployment_config_dir = self.get_deployment_dir_path(deployment_id);
@@ -103,9 +136,12 @@ impl FsHandler for FsHandlerImpl {
let full_output_path = self
.get_deployment_config_path(deployment_id, output_path, true)
.await?;
let parent_dir = full_output_path
.parent()
.context("Failed to get parent directory of the config file")?;
let parent_dir = full_output_path.parent().ok_or_else(|| {
FsHandlerError::InvalidOutputPath(format!(
"Failed to get parent directory of output path: {:?}",
full_output_path
))
})?;
// ensure the parent directory exists before creating the file
tokio::fs::create_dir_all(parent_dir).await?;
let mut file = tokio::fs::OpenOptions::new()