334 lines
9.8 KiB
Rust
334 lines
9.8 KiB
Rust
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 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>();
|
|
}
|
|
}
|