refactor: settings into modules
This commit is contained in:
@@ -1,560 +0,0 @@
|
|||||||
use config::{Config, ConfigError, Environment, File};
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
|
||||||
use std::{os::unix::fs::PermissionsExt, str::FromStr};
|
|
||||||
use tracing::level_filters::LevelFilter;
|
|
||||||
|
|
||||||
const NGINX_BINARY_PATH_TEMPLATE: &str = "{{nginx_binary_path}}";
|
|
||||||
const NGINX_DEFAULT_BINARY: &str = "nginx";
|
|
||||||
|
|
||||||
type ValidationError = String;
|
|
||||||
|
|
||||||
trait Validate {
|
|
||||||
fn validate(&self) -> Result<(), ValidationError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Agent settings
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Settings {
|
|
||||||
pub grpc: GrpcSettings,
|
|
||||||
#[serde(default)]
|
|
||||||
pub log: LogSettings,
|
|
||||||
pub nginx: Option<NginxSettings>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// gRPC client settings
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct GrpcSettings {
|
|
||||||
pub connection_string: String,
|
|
||||||
pub m_auth: MAuthSettings,
|
|
||||||
#[serde(default)]
|
|
||||||
pub cors: Option<CorsSettings>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum MAuthSettings {
|
|
||||||
Tls(TLSSettings),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TLS certificate settings
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum TLSSettings {
|
|
||||||
RawPath {
|
|
||||||
ca_path: String,
|
|
||||||
cert_path: String,
|
|
||||||
key_path: String,
|
|
||||||
},
|
|
||||||
ZipPath {
|
|
||||||
cert_zip_path: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CORS settings
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct CorsSettings {
|
|
||||||
#[serde(default)]
|
|
||||||
pub allowed_origins: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub allowed_methods: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub allowed_headers: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub allow_credentials: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Logging settings
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LogSettings {
|
|
||||||
#[serde(
|
|
||||||
deserialize_with = "deserialize_level_filter",
|
|
||||||
serialize_with = "serialize_level_filter"
|
|
||||||
)]
|
|
||||||
pub level: LevelFilter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LogSettings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
level: default_log_level(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct NginxSettings {
|
|
||||||
#[serde(default = "default_nginx_config_path")]
|
|
||||||
pub nginx_config_path: String,
|
|
||||||
// #[serde(default = "default_nginx_binary_path")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub nginx_binary_path: Option<String>,
|
|
||||||
// commands
|
|
||||||
#[serde(default = "default_nginx_reload_command")]
|
|
||||||
pub override_nginx_reload_command: Vec<String>,
|
|
||||||
#[serde(default = "default_nginx_test_command")]
|
|
||||||
pub override_nginx_test_command: Vec<String>,
|
|
||||||
// timeouts
|
|
||||||
#[serde(default = "default_nginx_reload_timeout_seconds")]
|
|
||||||
pub nginx_reload_timeout_seconds: u64,
|
|
||||||
#[serde(default = "default_nginx_test_timeout_seconds")]
|
|
||||||
pub nginx_test_timeout_seconds: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate for Settings {
|
|
||||||
fn validate(&self) -> Result<(), ValidationError> {
|
|
||||||
self.grpc.validate()?;
|
|
||||||
if let Some(nginx) = &self.nginx {
|
|
||||||
nginx.validate()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Settings {
|
|
||||||
/// Load settings from config files and environment
|
|
||||||
pub fn load() -> Result<Self, ConfigError> {
|
|
||||||
let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
|
|
||||||
|
|
||||||
let settings = Config::builder()
|
|
||||||
.add_source(File::with_name("config/default").required(false))
|
|
||||||
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
|
|
||||||
.add_source(File::with_name("config/agent/default").required(false))
|
|
||||||
.add_source(File::with_name(&format!("config/agent/{}", run_mode)).required(false))
|
|
||||||
.add_source(Environment::with_prefix("NXMESH").separator("__"))
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let mut settings: Self = settings.try_deserialize()?;
|
|
||||||
|
|
||||||
settings.validate().map_err(ConfigError::Message)?;
|
|
||||||
|
|
||||||
if let Some(nginx) = &mut settings.nginx {
|
|
||||||
nginx.validate().map_err(ConfigError::Message)?;
|
|
||||||
|
|
||||||
// replace binary path template in commands with actual binary path, if the template is present
|
|
||||||
nginx
|
|
||||||
.override_nginx_reload_command
|
|
||||||
.iter_mut()
|
|
||||||
.for_each(|cmd| {
|
|
||||||
*cmd = cmd.replace(
|
|
||||||
NGINX_BINARY_PATH_TEMPLATE,
|
|
||||||
&nginx
|
|
||||||
.nginx_binary_path
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
nginx
|
|
||||||
.override_nginx_test_command
|
|
||||||
.iter_mut()
|
|
||||||
.for_each(|cmd| {
|
|
||||||
*cmd = cmd.replace(
|
|
||||||
NGINX_BINARY_PATH_TEMPLATE,
|
|
||||||
&nginx
|
|
||||||
.nginx_binary_path
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(settings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate for GrpcSettings {
|
|
||||||
fn validate(&self) -> Result<(), ValidationError> {
|
|
||||||
if self.connection_string.is_empty() {
|
|
||||||
return Err("gRPC connection string cannot be empty".into());
|
|
||||||
}
|
|
||||||
self.m_auth.validate()?;
|
|
||||||
if let Some(cors) = &self.cors {
|
|
||||||
cors.validate()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate for MAuthSettings {
|
|
||||||
fn validate(&self) -> Result<(), ValidationError> {
|
|
||||||
match self {
|
|
||||||
MAuthSettings::Tls(tls_settings) => tls_settings.validate()?,
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate for TLSSettings {
|
|
||||||
fn validate(&self) -> Result<(), ValidationError> {
|
|
||||||
match self {
|
|
||||||
TLSSettings::RawPath {
|
|
||||||
ca_path,
|
|
||||||
cert_path,
|
|
||||||
key_path,
|
|
||||||
} => {
|
|
||||||
if !std::path::Path::new(ca_path).exists() {
|
|
||||||
return Err(format!("CA file not found: {}", ca_path));
|
|
||||||
}
|
|
||||||
if !std::path::Path::new(cert_path).exists() {
|
|
||||||
return Err(format!("Certificate file not found: {}", cert_path));
|
|
||||||
}
|
|
||||||
if !std::path::Path::new(key_path).exists() {
|
|
||||||
return Err(format!("Key file not found: {}", key_path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TLSSettings::ZipPath { cert_zip_path } => {
|
|
||||||
if !std::path::Path::new(cert_zip_path).exists() {
|
|
||||||
return Err(format!("Certificate zip file not found: {}", cert_zip_path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate for CorsSettings {
|
|
||||||
fn validate(&self) -> Result<(), ValidationError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate for NginxSettings {
|
|
||||||
fn validate(&self) -> Result<(), ValidationError> {
|
|
||||||
match &self.nginx_binary_path {
|
|
||||||
Some(path) if path.is_empty() => {
|
|
||||||
return Err("Nginx binary path cannot be empty".into());
|
|
||||||
}
|
|
||||||
Some(path) if !std::path::Path::new(path).exists() => {
|
|
||||||
return Err(format!("Nginx binary not found: {}", path));
|
|
||||||
}
|
|
||||||
Some(path)
|
|
||||||
if !std::fs::metadata(path)
|
|
||||||
.map_err(|e| format!("Failed to read nginx binary metadata: {}", e))?
|
|
||||||
.permissions()
|
|
||||||
.mode()
|
|
||||||
& 0o111
|
|
||||||
!= 0 =>
|
|
||||||
{
|
|
||||||
return Err(format!("Nginx binary is not executable: {}", path));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if self.nginx_config_path.is_empty() {
|
|
||||||
return Err("Nginx config path cannot be empty".into());
|
|
||||||
}
|
|
||||||
if !std::path::Path::new(&self.nginx_config_path).exists() {
|
|
||||||
return Err(format!(
|
|
||||||
"Nginx config file not found: {}",
|
|
||||||
self.nginx_config_path
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure reload and test commands contain the binary path template
|
|
||||||
if !&self
|
|
||||||
.override_nginx_reload_command
|
|
||||||
.join(" ")
|
|
||||||
.contains(NGINX_BINARY_PATH_TEMPLATE)
|
|
||||||
{
|
|
||||||
return Err(format!(
|
|
||||||
"Nginx reload command must contain the binary path template '{}': {}",
|
|
||||||
NGINX_BINARY_PATH_TEMPLATE,
|
|
||||||
self.override_nginx_reload_command.join(" ")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if !&self
|
|
||||||
.override_nginx_test_command
|
|
||||||
.join(" ")
|
|
||||||
.contains(NGINX_BINARY_PATH_TEMPLATE)
|
|
||||||
{
|
|
||||||
return Err(format!(
|
|
||||||
"Nginx test command must contain the binary path template '{}': {}",
|
|
||||||
NGINX_BINARY_PATH_TEMPLATE,
|
|
||||||
self.override_nginx_test_command.join(" ")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_log_level() -> LevelFilter {
|
|
||||||
LevelFilter::INFO
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_nginx_config_path() -> String {
|
|
||||||
"/etc/nginx/nginx.conf".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_nginx_reload_command() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
NGINX_BINARY_PATH_TEMPLATE.to_string(),
|
|
||||||
"-s".to_string(),
|
|
||||||
"reload".to_string(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_nginx_test_command() -> Vec<String> {
|
|
||||||
vec![NGINX_BINARY_PATH_TEMPLATE.to_string(), "-t".to_string()]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_nginx_reload_timeout_seconds() -> u64 {
|
|
||||||
30
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_nginx_test_timeout_seconds() -> u64 {
|
|
||||||
30
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_level_filter<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
LevelFilter::from_str(&s).map_err(serde::de::Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_level_filter<S>(level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&level.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
os::unix::fs::PermissionsExt,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_esnure_send_and_sync() {
|
|
||||||
fn assert_send_sync<T: Send + Sync>() {}
|
|
||||||
assert_send_sync::<Settings>();
|
|
||||||
assert_send_sync::<GrpcSettings>();
|
|
||||||
assert_send_sync::<TLSSettings>();
|
|
||||||
assert_send_sync::<CorsSettings>();
|
|
||||||
assert_send_sync::<LogSettings>();
|
|
||||||
assert_send_sync::<NginxSettings>();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_file(path: &Path) {
|
|
||||||
let result = fs::write(path, b"content");
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_exec_file(path: &Path) {
|
|
||||||
write_file(path);
|
|
||||||
let metadata = fs::metadata(path);
|
|
||||||
assert!(metadata.is_ok());
|
|
||||||
let metadata = metadata.ok();
|
|
||||||
assert!(metadata.is_some());
|
|
||||||
let metadata = metadata.unwrap_or_else(|| unreachable!());
|
|
||||||
|
|
||||||
let mut perms = metadata.permissions();
|
|
||||||
perms.set_mode(0o755);
|
|
||||||
let result = fs::set_permissions(path, perms);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_non_exec_file(path: &Path) {
|
|
||||||
write_file(path);
|
|
||||||
let metadata = fs::metadata(path);
|
|
||||||
assert!(metadata.is_ok());
|
|
||||||
let metadata = metadata.ok();
|
|
||||||
assert!(metadata.is_some());
|
|
||||||
let metadata = metadata.unwrap_or_else(|| unreachable!());
|
|
||||||
|
|
||||||
let mut perms = metadata.permissions();
|
|
||||||
perms.set_mode(0o644);
|
|
||||||
let result = fs::set_permissions(path, perms);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valid_tls_raw_paths(temp_dir: &TempDir) -> (PathBuf, PathBuf, PathBuf) {
|
|
||||||
let ca_path = temp_dir.path().join("ca.pem");
|
|
||||||
let cert_path = temp_dir.path().join("cert.pem");
|
|
||||||
let key_path = temp_dir.path().join("key.pem");
|
|
||||||
|
|
||||||
write_file(&ca_path);
|
|
||||||
write_file(&cert_path);
|
|
||||||
write_file(&key_path);
|
|
||||||
|
|
||||||
(ca_path, cert_path, key_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tls_raw_path_validate_succeeds_when_all_files_exist() {
|
|
||||||
let temp_dir = TempDir::new();
|
|
||||||
assert!(temp_dir.is_ok());
|
|
||||||
let temp_dir = temp_dir.ok();
|
|
||||||
assert!(temp_dir.is_some());
|
|
||||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
|
||||||
|
|
||||||
let (ca_path, cert_path, key_path) = valid_tls_raw_paths(&temp_dir);
|
|
||||||
let settings = TLSSettings::RawPath {
|
|
||||||
ca_path: ca_path.to_string_lossy().to_string(),
|
|
||||||
cert_path: cert_path.to_string_lossy().to_string(),
|
|
||||||
key_path: key_path.to_string_lossy().to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(settings.validate().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tls_raw_path_validate_fails_when_ca_missing() {
|
|
||||||
let settings = TLSSettings::RawPath {
|
|
||||||
ca_path: "/tmp/does-not-exist-ca.pem".into(),
|
|
||||||
cert_path: "/tmp/does-not-exist-cert.pem".into(),
|
|
||||||
key_path: "/tmp/does-not-exist-key.pem".into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = settings.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
|
||||||
assert!(msg.contains("CA file not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tls_zip_path_validate_fails_when_zip_missing() {
|
|
||||||
let settings = TLSSettings::ZipPath {
|
|
||||||
cert_zip_path: "/tmp/missing-certs.zip".into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = settings.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
|
||||||
assert!(msg.contains("Certificate zip file not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn grpc_validate_fails_when_connection_string_empty() {
|
|
||||||
let settings = GrpcSettings {
|
|
||||||
connection_string: "".into(),
|
|
||||||
m_auth: MAuthSettings::Tls(TLSSettings::ZipPath {
|
|
||||||
cert_zip_path: "/tmp/does-not-exist.zip".into(),
|
|
||||||
}),
|
|
||||||
cors: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = settings.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
|
||||||
assert!(msg.contains("gRPC connection string cannot be empty"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn nginx_validate_succeeds_for_valid_paths_and_commands() {
|
|
||||||
let temp_dir = TempDir::new();
|
|
||||||
assert!(temp_dir.is_ok());
|
|
||||||
let temp_dir = temp_dir.ok();
|
|
||||||
assert!(temp_dir.is_some());
|
|
||||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
|
||||||
|
|
||||||
let nginx_binary = temp_dir.path().join("nginx");
|
|
||||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
|
||||||
|
|
||||||
create_exec_file(&nginx_binary);
|
|
||||||
write_file(&nginx_config);
|
|
||||||
|
|
||||||
let nginx = NginxSettings {
|
|
||||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
|
||||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
|
||||||
override_nginx_reload_command: default_nginx_reload_command(),
|
|
||||||
override_nginx_test_command: default_nginx_test_command(),
|
|
||||||
nginx_reload_timeout_seconds: 30,
|
|
||||||
nginx_test_timeout_seconds: 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(nginx.validate().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn nginx_validate_fails_for_non_executable_binary() {
|
|
||||||
let temp_dir = TempDir::new();
|
|
||||||
assert!(temp_dir.is_ok());
|
|
||||||
let temp_dir = temp_dir.ok();
|
|
||||||
assert!(temp_dir.is_some());
|
|
||||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
|
||||||
|
|
||||||
let nginx_binary = temp_dir.path().join("nginx");
|
|
||||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
|
||||||
|
|
||||||
create_non_exec_file(&nginx_binary);
|
|
||||||
write_file(&nginx_config);
|
|
||||||
|
|
||||||
let nginx = NginxSettings {
|
|
||||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
|
||||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
|
||||||
override_nginx_reload_command: default_nginx_reload_command(),
|
|
||||||
override_nginx_test_command: default_nginx_test_command(),
|
|
||||||
nginx_reload_timeout_seconds: 30,
|
|
||||||
nginx_test_timeout_seconds: 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = nginx.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
|
||||||
assert!(msg.contains("Nginx binary is not executable"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn nginx_validate_fails_when_reload_command_lacks_template() {
|
|
||||||
let temp_dir = TempDir::new();
|
|
||||||
assert!(temp_dir.is_ok());
|
|
||||||
let temp_dir = temp_dir.ok();
|
|
||||||
assert!(temp_dir.is_some());
|
|
||||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
|
||||||
|
|
||||||
let nginx_binary = temp_dir.path().join("nginx");
|
|
||||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
|
||||||
|
|
||||||
create_exec_file(&nginx_binary);
|
|
||||||
write_file(&nginx_config);
|
|
||||||
|
|
||||||
let nginx = NginxSettings {
|
|
||||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
|
||||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
|
||||||
override_nginx_reload_command: vec!["nginx".into(), "-s".into(), "reload".into()],
|
|
||||||
override_nginx_test_command: default_nginx_test_command(),
|
|
||||||
nginx_reload_timeout_seconds: 30,
|
|
||||||
nginx_test_timeout_seconds: 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = nginx.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
|
||||||
assert!(msg.contains("Nginx reload command must contain the binary path template"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn level_filter_round_trip_serialization() {
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct Wrapper {
|
|
||||||
#[serde(
|
|
||||||
deserialize_with = "deserialize_level_filter",
|
|
||||||
serialize_with = "serialize_level_filter"
|
|
||||||
)]
|
|
||||||
level: LevelFilter,
|
|
||||||
}
|
|
||||||
|
|
||||||
let original = Wrapper {
|
|
||||||
level: LevelFilter::DEBUG,
|
|
||||||
};
|
|
||||||
|
|
||||||
let encoded = serde_json::to_string(&original);
|
|
||||||
assert!(encoded.is_ok());
|
|
||||||
let encoded = encoded.ok();
|
|
||||||
assert!(encoded.is_some());
|
|
||||||
let encoded = encoded.unwrap_or_else(|| unreachable!());
|
|
||||||
assert!(encoded.to_lowercase().contains("debug"));
|
|
||||||
|
|
||||||
let decoded = serde_json::from_str::<Wrapper>(&encoded);
|
|
||||||
assert!(decoded.is_ok());
|
|
||||||
let decoded = decoded.ok();
|
|
||||||
assert!(decoded.is_some());
|
|
||||||
let decoded = decoded.unwrap_or_else(|| unreachable!());
|
|
||||||
assert_eq!(decoded.level, LevelFilter::DEBUG);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
166
apps/nxmesh-agent/src/config/settings/auth.rs
Normal file
166
apps/nxmesh-agent/src/config/settings/auth.rs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::settings::{Validate, ValidationError};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum MAuthSettings {
|
||||||
|
Tls(TLSSettings),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TLS certificate settings
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum TLSSettings {
|
||||||
|
RawPath {
|
||||||
|
ca_path: String,
|
||||||
|
cert_path: String,
|
||||||
|
key_path: String,
|
||||||
|
},
|
||||||
|
ZipPath {
|
||||||
|
cert_zip_path: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for MAuthSettings {
|
||||||
|
fn validate(&self) -> Result<(), ValidationError> {
|
||||||
|
match self {
|
||||||
|
MAuthSettings::Tls(tls_settings) => tls_settings.validate()?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for TLSSettings {
|
||||||
|
fn validate(&self) -> Result<(), ValidationError> {
|
||||||
|
match self {
|
||||||
|
TLSSettings::RawPath {
|
||||||
|
ca_path,
|
||||||
|
cert_path,
|
||||||
|
key_path,
|
||||||
|
} => {
|
||||||
|
if !std::path::Path::new(ca_path).exists() {
|
||||||
|
return Err(format!("CA file not found: {}", ca_path));
|
||||||
|
}
|
||||||
|
if !std::path::Path::new(cert_path).exists() {
|
||||||
|
return Err(format!("Certificate file not found: {}", cert_path));
|
||||||
|
}
|
||||||
|
if !std::path::Path::new(key_path).exists() {
|
||||||
|
return Err(format!("Key file not found: {}", key_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TLSSettings::ZipPath { cert_zip_path } => {
|
||||||
|
if !std::path::Path::new(cert_zip_path).exists() {
|
||||||
|
return Err(format!("Certificate zip file not found: {}", cert_zip_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_esnure_send_and_sync() {
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
assert_send_sync::<TLSSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_file(path: &Path) {
|
||||||
|
let result = fs::write(path, b"content");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_exec_file(path: &Path) {
|
||||||
|
write_file(path);
|
||||||
|
let metadata = fs::metadata(path);
|
||||||
|
assert!(metadata.is_ok());
|
||||||
|
let metadata = metadata.ok();
|
||||||
|
assert!(metadata.is_some());
|
||||||
|
let metadata = metadata.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let mut perms = metadata.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
let result = fs::set_permissions(path, perms);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_non_exec_file(path: &Path) {
|
||||||
|
write_file(path);
|
||||||
|
let metadata = fs::metadata(path);
|
||||||
|
assert!(metadata.is_ok());
|
||||||
|
let metadata = metadata.ok();
|
||||||
|
assert!(metadata.is_some());
|
||||||
|
let metadata = metadata.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let mut perms = metadata.permissions();
|
||||||
|
perms.set_mode(0o644);
|
||||||
|
let result = fs::set_permissions(path, perms);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_tls_raw_paths(temp_dir: &TempDir) -> (PathBuf, PathBuf, PathBuf) {
|
||||||
|
let ca_path = temp_dir.path().join("ca.pem");
|
||||||
|
let cert_path = temp_dir.path().join("cert.pem");
|
||||||
|
let key_path = temp_dir.path().join("key.pem");
|
||||||
|
|
||||||
|
write_file(&ca_path);
|
||||||
|
write_file(&cert_path);
|
||||||
|
write_file(&key_path);
|
||||||
|
|
||||||
|
(ca_path, cert_path, key_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_raw_path_validate_succeeds_when_all_files_exist() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
assert!(temp_dir.is_ok());
|
||||||
|
let temp_dir = temp_dir.ok();
|
||||||
|
assert!(temp_dir.is_some());
|
||||||
|
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let (ca_path, cert_path, key_path) = valid_tls_raw_paths(&temp_dir);
|
||||||
|
let settings = TLSSettings::RawPath {
|
||||||
|
ca_path: ca_path.to_string_lossy().to_string(),
|
||||||
|
cert_path: cert_path.to_string_lossy().to_string(),
|
||||||
|
key_path: key_path.to_string_lossy().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_raw_path_validate_fails_when_ca_missing() {
|
||||||
|
let settings = TLSSettings::RawPath {
|
||||||
|
ca_path: "/tmp/does-not-exist-ca.pem".into(),
|
||||||
|
cert_path: "/tmp/does-not-exist-cert.pem".into(),
|
||||||
|
key_path: "/tmp/does-not-exist-key.pem".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = settings.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("CA file not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_zip_path_validate_fails_when_zip_missing() {
|
||||||
|
let settings = TLSSettings::ZipPath {
|
||||||
|
cert_zip_path: "/tmp/missing-certs.zip".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = settings.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("Certificate zip file not found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
34
apps/nxmesh-agent/src/config/settings/cors.rs
Normal file
34
apps/nxmesh-agent/src/config/settings/cors.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::settings::{Validate, ValidationError};
|
||||||
|
|
||||||
|
/// CORS settings
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct CorsSettings {
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_origins: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_methods: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_headers: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub allow_credentials: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for CorsSettings {
|
||||||
|
fn validate(&self) -> Result<(), ValidationError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_esnure_send_and_sync() {
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
assert_send_sync::<CorsSettings>();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
apps/nxmesh-agent/src/config/settings/grpc.rs
Normal file
56
apps/nxmesh-agent/src/config/settings/grpc.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::super::settings::{Validate, ValidationError};
|
||||||
|
use super::{auth::MAuthSettings, cors::CorsSettings};
|
||||||
|
|
||||||
|
/// gRPC client settings
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GrpcSettings {
|
||||||
|
pub connection_string: String,
|
||||||
|
pub m_auth: MAuthSettings,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cors: Option<CorsSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for GrpcSettings {
|
||||||
|
fn validate(&self) -> Result<(), ValidationError> {
|
||||||
|
if self.connection_string.is_empty() {
|
||||||
|
return Err("gRPC connection string cannot be empty".into());
|
||||||
|
}
|
||||||
|
self.m_auth.validate()?;
|
||||||
|
if let Some(cors) = &self.cors {
|
||||||
|
cors.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use crate::config::settings::TLSSettings;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_esnure_send_and_sync() {
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
assert_send_sync::<GrpcSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_validate_fails_when_connection_string_empty() {
|
||||||
|
let settings = GrpcSettings {
|
||||||
|
connection_string: "".into(),
|
||||||
|
m_auth: MAuthSettings::Tls(TLSSettings::ZipPath {
|
||||||
|
cert_zip_path: "/tmp/does-not-exist.zip".into(),
|
||||||
|
}),
|
||||||
|
cors: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = settings.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("gRPC connection string cannot be empty"));
|
||||||
|
}
|
||||||
|
}
|
||||||
82
apps/nxmesh-agent/src/config/settings/log.rs
Normal file
82
apps/nxmesh-agent/src/config/settings/log.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use tracing::level_filters::LevelFilter;
|
||||||
|
|
||||||
|
/// Logging settings
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LogSettings {
|
||||||
|
#[serde(
|
||||||
|
deserialize_with = "deserialize_level_filter",
|
||||||
|
serialize_with = "serialize_level_filter"
|
||||||
|
)]
|
||||||
|
pub level: LevelFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
level: default_log_level(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_log_level() -> LevelFilter {
|
||||||
|
LevelFilter::INFO
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_level_filter<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
LevelFilter::from_str(&s).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_level_filter<S>(level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&level.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_esnure_send_and_sync() {
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
assert_send_sync::<LogSettings>();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn level_filter_round_trip_serialization() {
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct Wrapper {
|
||||||
|
#[serde(
|
||||||
|
deserialize_with = "deserialize_level_filter",
|
||||||
|
serialize_with = "serialize_level_filter"
|
||||||
|
)]
|
||||||
|
level: LevelFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
let original = Wrapper {
|
||||||
|
level: LevelFilter::DEBUG,
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded = serde_json::to_string(&original);
|
||||||
|
assert!(encoded.is_ok());
|
||||||
|
let encoded = encoded.ok();
|
||||||
|
assert!(encoded.is_some());
|
||||||
|
let encoded = encoded.unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(encoded.to_lowercase().contains("debug"));
|
||||||
|
|
||||||
|
let decoded = serde_json::from_str::<Wrapper>(&encoded);
|
||||||
|
assert!(decoded.is_ok());
|
||||||
|
let decoded = decoded.ok();
|
||||||
|
assert!(decoded.is_some());
|
||||||
|
let decoded = decoded.unwrap_or_else(|| unreachable!());
|
||||||
|
assert_eq!(decoded.level, LevelFilter::DEBUG);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
apps/nxmesh-agent/src/config/settings/mod.rs
Normal file
77
apps/nxmesh-agent/src/config/settings/mod.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use config::{Config, ConfigError, Environment, File};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
mod auth;
|
||||||
|
mod cors;
|
||||||
|
mod grpc;
|
||||||
|
mod log;
|
||||||
|
mod nginx;
|
||||||
|
|
||||||
|
pub use auth::*;
|
||||||
|
pub use cors::*;
|
||||||
|
pub use grpc::*;
|
||||||
|
pub use log::*;
|
||||||
|
pub use nginx::*;
|
||||||
|
|
||||||
|
pub type ValidationError = String;
|
||||||
|
|
||||||
|
pub trait Validate {
|
||||||
|
fn validate(&self) -> Result<(), ValidationError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Agent settings
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub grpc: GrpcSettings,
|
||||||
|
#[serde(default)]
|
||||||
|
pub log: LogSettings,
|
||||||
|
pub nginx: Option<NginxSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for Settings {
|
||||||
|
fn validate(&self) -> Result<(), ValidationError> {
|
||||||
|
self.grpc.validate()?;
|
||||||
|
if let Some(nginx) = &self.nginx {
|
||||||
|
nginx.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
/// Load settings from config files and environment
|
||||||
|
pub fn load() -> Result<Self, ConfigError> {
|
||||||
|
let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
|
||||||
|
|
||||||
|
let settings = Config::builder()
|
||||||
|
.add_source(File::with_name("config/default").required(false))
|
||||||
|
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
|
||||||
|
.add_source(File::with_name("config/agent/default").required(false))
|
||||||
|
.add_source(File::with_name(&format!("config/agent/{}", run_mode)).required(false))
|
||||||
|
.add_source(Environment::with_prefix("NXMESH").separator("__"))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let mut settings: Self = settings.try_deserialize()?;
|
||||||
|
|
||||||
|
settings.validate().map_err(ConfigError::Message)?;
|
||||||
|
|
||||||
|
if let Some(nginx) = &mut settings.nginx {
|
||||||
|
nginx.validate().map_err(ConfigError::Message)?;
|
||||||
|
nginx.transform_commands();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_send_and_sync() {
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
assert_send_sync::<Settings>();
|
||||||
|
}
|
||||||
|
}
|
||||||
280
apps/nxmesh-agent/src/config/settings/nginx.rs
Normal file
280
apps/nxmesh-agent/src/config/settings/nginx.rs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::settings::{Validate, ValidationError};
|
||||||
|
|
||||||
|
const NGINX_BINARY_PATH_TEMPLATE: &str = "{{nginx_binary_path}}";
|
||||||
|
const NGINX_DEFAULT_BINARY: &str = "nginx";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NginxSettings {
|
||||||
|
#[serde(default = "default_nginx_config_path")]
|
||||||
|
pub nginx_config_path: String,
|
||||||
|
// #[serde(default = "default_nginx_binary_path")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub nginx_binary_path: Option<String>,
|
||||||
|
// commands
|
||||||
|
#[serde(default = "default_nginx_reload_command")]
|
||||||
|
pub override_nginx_reload_command: Vec<String>,
|
||||||
|
#[serde(default = "default_nginx_test_command")]
|
||||||
|
pub override_nginx_test_command: Vec<String>,
|
||||||
|
// timeouts
|
||||||
|
#[serde(default = "default_nginx_reload_timeout_seconds")]
|
||||||
|
pub nginx_reload_timeout_seconds: u64,
|
||||||
|
#[serde(default = "default_nginx_test_timeout_seconds")]
|
||||||
|
pub nginx_test_timeout_seconds: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxSettings {
|
||||||
|
/// Transforms the reload and test commands by replacing the binary path template with the actual binary path if provided.
|
||||||
|
/// This MUST be called after validation to ensure the binary path is valid and the commands contain the template.
|
||||||
|
pub fn transform_commands(&mut self) {
|
||||||
|
self.override_nginx_reload_command = self.transformed_reload_command();
|
||||||
|
self.override_nginx_test_command = self.transformed_test_command();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transformed_reload_command(&self) -> Vec<String> {
|
||||||
|
self.override_nginx_reload_command
|
||||||
|
.iter()
|
||||||
|
.map(|cmd| {
|
||||||
|
cmd.replace(
|
||||||
|
NGINX_BINARY_PATH_TEMPLATE,
|
||||||
|
&self
|
||||||
|
.nginx_binary_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transformed_test_command(&self) -> Vec<String> {
|
||||||
|
self.override_nginx_test_command
|
||||||
|
.iter()
|
||||||
|
.map(|cmd| {
|
||||||
|
cmd.replace(
|
||||||
|
NGINX_BINARY_PATH_TEMPLATE,
|
||||||
|
&self
|
||||||
|
.nginx_binary_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for NginxSettings {
|
||||||
|
fn validate(&self) -> Result<(), ValidationError> {
|
||||||
|
match &self.nginx_binary_path {
|
||||||
|
Some(path) if path.is_empty() => {
|
||||||
|
return Err("Nginx binary path cannot be empty".into());
|
||||||
|
}
|
||||||
|
Some(path) if !std::path::Path::new(path).exists() => {
|
||||||
|
return Err(format!("Nginx binary not found: {}", path));
|
||||||
|
}
|
||||||
|
Some(path)
|
||||||
|
if !std::fs::metadata(path)
|
||||||
|
.map_err(|e| format!("Failed to read nginx binary metadata: {}", e))?
|
||||||
|
.permissions()
|
||||||
|
.mode()
|
||||||
|
& 0o111
|
||||||
|
!= 0 =>
|
||||||
|
{
|
||||||
|
return Err(format!("Nginx binary is not executable: {}", path));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if self.nginx_config_path.is_empty() {
|
||||||
|
return Err("Nginx config path cannot be empty".into());
|
||||||
|
}
|
||||||
|
if !std::path::Path::new(&self.nginx_config_path).exists() {
|
||||||
|
return Err(format!(
|
||||||
|
"Nginx config file not found: {}",
|
||||||
|
self.nginx_config_path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure reload and test commands contain the binary path template
|
||||||
|
if !&self
|
||||||
|
.override_nginx_reload_command
|
||||||
|
.join(" ")
|
||||||
|
.contains(NGINX_BINARY_PATH_TEMPLATE)
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"Nginx reload command must contain the binary path template '{}': {}",
|
||||||
|
NGINX_BINARY_PATH_TEMPLATE,
|
||||||
|
self.override_nginx_reload_command.join(" ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !&self
|
||||||
|
.override_nginx_test_command
|
||||||
|
.join(" ")
|
||||||
|
.contains(NGINX_BINARY_PATH_TEMPLATE)
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"Nginx test command must contain the binary path template '{}': {}",
|
||||||
|
NGINX_BINARY_PATH_TEMPLATE,
|
||||||
|
self.override_nginx_test_command.join(" ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_nginx_config_path() -> String {
|
||||||
|
"/etc/nginx/nginx.conf".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_nginx_reload_command() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
NGINX_BINARY_PATH_TEMPLATE.to_string(),
|
||||||
|
"-s".to_string(),
|
||||||
|
"reload".to_string(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_nginx_test_command() -> Vec<String> {
|
||||||
|
vec![NGINX_BINARY_PATH_TEMPLATE.to_string(), "-t".to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_nginx_reload_timeout_seconds() -> u64 {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_nginx_test_timeout_seconds() -> u64 {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{fs, os::unix::fs::PermissionsExt, path::Path};
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_esnure_send_and_sync() {
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
assert_send_sync::<NginxSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_file(path: &Path) {
|
||||||
|
let result = fs::write(path, b"content");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_exec_file(path: &Path) {
|
||||||
|
write_file(path);
|
||||||
|
let metadata = fs::metadata(path);
|
||||||
|
assert!(metadata.is_ok());
|
||||||
|
let metadata = metadata.ok();
|
||||||
|
assert!(metadata.is_some());
|
||||||
|
let metadata = metadata.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let mut perms = metadata.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
let result = fs::set_permissions(path, perms);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_non_exec_file(path: &Path) {
|
||||||
|
write_file(path);
|
||||||
|
let metadata = fs::metadata(path);
|
||||||
|
assert!(metadata.is_ok());
|
||||||
|
let metadata = metadata.ok();
|
||||||
|
assert!(metadata.is_some());
|
||||||
|
let metadata = metadata.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let mut perms = metadata.permissions();
|
||||||
|
perms.set_mode(0o644);
|
||||||
|
let result = fs::set_permissions(path, perms);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nginx_validate_succeeds_for_valid_paths_and_commands() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
assert!(temp_dir.is_ok());
|
||||||
|
let temp_dir = temp_dir.ok();
|
||||||
|
assert!(temp_dir.is_some());
|
||||||
|
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let nginx_binary = temp_dir.path().join("nginx");
|
||||||
|
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||||
|
|
||||||
|
create_exec_file(&nginx_binary);
|
||||||
|
write_file(&nginx_config);
|
||||||
|
|
||||||
|
let nginx = NginxSettings {
|
||||||
|
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||||
|
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||||
|
override_nginx_reload_command: default_nginx_reload_command(),
|
||||||
|
override_nginx_test_command: default_nginx_test_command(),
|
||||||
|
nginx_reload_timeout_seconds: 30,
|
||||||
|
nginx_test_timeout_seconds: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(nginx.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nginx_validate_fails_for_non_executable_binary() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
assert!(temp_dir.is_ok());
|
||||||
|
let temp_dir = temp_dir.ok();
|
||||||
|
assert!(temp_dir.is_some());
|
||||||
|
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let nginx_binary = temp_dir.path().join("nginx");
|
||||||
|
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||||
|
|
||||||
|
create_non_exec_file(&nginx_binary);
|
||||||
|
write_file(&nginx_config);
|
||||||
|
|
||||||
|
let nginx = NginxSettings {
|
||||||
|
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||||
|
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||||
|
override_nginx_reload_command: default_nginx_reload_command(),
|
||||||
|
override_nginx_test_command: default_nginx_test_command(),
|
||||||
|
nginx_reload_timeout_seconds: 30,
|
||||||
|
nginx_test_timeout_seconds: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = nginx.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("Nginx binary is not executable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nginx_validate_fails_when_reload_command_lacks_template() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
assert!(temp_dir.is_ok());
|
||||||
|
let temp_dir = temp_dir.ok();
|
||||||
|
assert!(temp_dir.is_some());
|
||||||
|
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let nginx_binary = temp_dir.path().join("nginx");
|
||||||
|
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||||
|
|
||||||
|
create_exec_file(&nginx_binary);
|
||||||
|
write_file(&nginx_config);
|
||||||
|
|
||||||
|
let nginx = NginxSettings {
|
||||||
|
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||||
|
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||||
|
override_nginx_reload_command: vec!["nginx".into(), "-s".into(), "reload".into()],
|
||||||
|
override_nginx_test_command: default_nginx_test_command(),
|
||||||
|
nginx_reload_timeout_seconds: 30,
|
||||||
|
nginx_test_timeout_seconds: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = nginx.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("Nginx reload command must contain the binary path template"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user