feat: add agent module with Nginx service commands and routes

- Introduced a new agent module with commands for managing Nginx configurations.
- Implemented `NginxService` for handling reload, validation, and configuration writing.
- Added routes for status, validation, and configuration writing using Axum.
- Created necessary command files: `reload.rs`, `run.rs`, `validate.rs`, `write_config.rs`.
- Updated `Cargo.toml` and `Cargo.lock` to include new dependencies.
- Added `.gitignore` for the agent module.
- Updated `justfile` to include OpenAPI generation for the agent.
This commit is contained in:
GW_MC
2025-12-21 15:32:42 +08:00
parent 8334da8cf1
commit 4ca59d2bb6
12 changed files with 1023 additions and 3 deletions

View File

@@ -0,0 +1,85 @@
use std::time::Duration;
use tokio::{process::Command, time::timeout};
use tracing::error;
pub fn to_file_name(
config_name: &str,
timestamp: u64,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// reject empty or unsafe names to avoid path traversal or invalid filesystem chars
if config_name.is_empty() {
return Err("config_name is empty".into());
}
if config_name.len() > 255 {
return Err("config_name too long".into());
}
if config_name.contains('/') || config_name.contains('\\') || config_name.contains("..") {
return Err("config_name contains invalid path characters".into());
}
if !config_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-._".contains(c))
{
return Err("config_name contains invalid characters".into());
}
Ok(format!("{}_{}.conf", timestamp, config_name))
}
pub async fn run_cmd(
cmd: &str,
args: &[&str],
dur_s: u64,
) -> Result<(i32, String), Box<dyn std::error::Error + Send + Sync>> {
let mut c = Command::new(cmd);
c.args(args);
let res = timeout(Duration::from_secs(dur_s), c.output()).await;
let out = match res {
Ok(Ok(out)) => out,
Ok(Err(e)) => return Err(Box::new(e)),
Err(_) => {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"command timeout",
)));
}
};
let code = out.status.code().unwrap_or(-1);
let output = String::from_utf8_lossy(&[out.stdout, out.stderr].concat()).to_string();
if code != 0 {
error!("command failed ({}): {}", code, output);
return Err(format!("command failed ({}): {}", code, output).into());
}
Ok((code, output))
}
#[cfg(test)]
mod tests {
use super::to_file_name;
#[test]
fn to_file_name_valid() {
let res = to_file_name("myconf", 1234).expect("should succeed");
assert_eq!(res, "1234_myconf.conf");
}
#[test]
fn to_file_name_empty() {
assert!(to_file_name("", 1).is_err());
}
#[test]
fn to_file_name_invalid_chars() {
assert!(to_file_name("bad/name", 1).is_err());
assert!(to_file_name("bad\\name", 1).is_err());
assert!(to_file_name("bad..name", 1).is_err());
assert!(to_file_name("bad$name", 1).is_err());
}
#[test]
fn to_file_name_too_long() {
let long = "a".repeat(300);
assert!(to_file_name(&long, 1).is_err());
}
}