- Added SystemHealthChecker to collect CPU, memory, disk, and load average metrics. - Implemented methods to retrieve system information from /proc filesystem. - Introduced a new method to collect metrics and return a SystemMetrics struct. - Added tests for metric collection and parsing functions. feat: Enhance agent runtime with state management and health monitoring - Created AgentState struct to manage agent state with RwLock for concurrency. - Refactored agent start logic to initialize cache and start health monitoring. - Implemented connection loop with reconnection logic and health report handling. - Added config update handling with nginx controller integration. feat: Expand master client functionality for bidirectional streaming - Updated MasterClient to support bidirectional streaming for health reports. - Implemented registration logic with the master server. - Added methods for sending messages and managing connection state. feat: Improve nginx configuration management and rendering - Enhanced ConfigManager for atomic symlink swaps and configuration validation. - Implemented ConfigRenderer using Handlebars for dynamic nginx configuration generation. - Added methods for applying, rolling back, and cleaning up configurations. - Introduced tests for configuration rendering and validation. feat: Implement nginx process control with lifecycle management - Added methods to start, stop, reload, and test nginx configurations. - Implemented graceful and immediate stop functionality. - Enhanced error handling and logging for nginx operations. - Added tests for deployment mode parsing and nginx lifecycle management.
337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
//! Nginx configuration renderer using Handlebars templates
|
|
|
|
use handlebars::Handlebars;
|
|
use serde::Serialize;
|
|
use tracing::{debug, error};
|
|
|
|
/// Default virtual host template
|
|
const DEFAULT_VHOST_TEMPLATE: &str = include_str!("templates/default.hbs");
|
|
|
|
/// Upstream template
|
|
const UPSTREAM_TEMPLATE: &str = r#"{{#each upstreams}}
|
|
upstream {{name}} {
|
|
{{#each servers}}
|
|
server {{address}}{{#if weight}} weight={{weight}}{{/if}}{{#if backup}} backup{{/if}}{{#if down}} down{{/if}};
|
|
{{/each}}
|
|
{{#if keepalive_connections}}
|
|
keepalive {{keepalive_connections}};
|
|
{{/if}}
|
|
}
|
|
{{/each}}
|
|
"#;
|
|
|
|
/// Configuration renderer
|
|
pub struct ConfigRenderer {
|
|
handlebars: Handlebars<'static>,
|
|
}
|
|
|
|
/// Virtual host data for template
|
|
#[derive(Serialize)]
|
|
struct VirtualHostData {
|
|
id: String,
|
|
name: String,
|
|
server_name: String,
|
|
listen_port: u32,
|
|
ssl_enabled: bool,
|
|
ssl_certificate_path: String,
|
|
ssl_certificate_key_path: String,
|
|
http2_enabled: bool,
|
|
locations: Vec<LocationData>,
|
|
}
|
|
|
|
/// Location data for template
|
|
#[derive(Serialize)]
|
|
struct LocationData {
|
|
path: String,
|
|
proxy_pass: String,
|
|
upstream_name: String,
|
|
root: String,
|
|
index: String,
|
|
custom_headers: Vec<HeaderData>,
|
|
}
|
|
|
|
/// Header data for template
|
|
#[derive(Serialize)]
|
|
struct HeaderData {
|
|
name: String,
|
|
value: String,
|
|
always: bool,
|
|
}
|
|
|
|
/// Upstream data for template
|
|
#[derive(Serialize)]
|
|
struct UpstreamData {
|
|
id: String,
|
|
name: String,
|
|
algorithm: String,
|
|
servers: Vec<UpstreamServerData>,
|
|
keepalive_connections: u32,
|
|
}
|
|
|
|
/// Upstream server data for template
|
|
#[derive(Serialize)]
|
|
struct UpstreamServerData {
|
|
address: String,
|
|
weight: u32,
|
|
backup: bool,
|
|
down: bool,
|
|
}
|
|
|
|
impl ConfigRenderer {
|
|
/// Create a new config renderer
|
|
pub fn new() -> Self {
|
|
let mut handlebars = Handlebars::new();
|
|
|
|
// Register built-in templates
|
|
handlebars
|
|
.register_template_string("default_vhost", DEFAULT_VHOST_TEMPLATE)
|
|
.expect("Failed to register default_vhost template");
|
|
handlebars
|
|
.register_template_string("upstreams", UPSTREAM_TEMPLATE)
|
|
.expect("Failed to register upstreams template");
|
|
|
|
Self { handlebars }
|
|
}
|
|
|
|
/// Render full configuration from ConfigUpdate
|
|
pub fn render_config(
|
|
&self,
|
|
config: &nxmesh_proto::ConfigUpdate,
|
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
|
debug!("Rendering configuration with Handlebars");
|
|
|
|
let mut output = String::new();
|
|
|
|
// Render upstreams
|
|
if !config.upstreams.is_empty() {
|
|
let upstreams_data: Vec<UpstreamData> = config
|
|
.upstreams
|
|
.iter()
|
|
.map(|u| UpstreamData {
|
|
id: u.id.clone(),
|
|
name: u.name.clone(),
|
|
algorithm: algorithm_name(u.algorithm).to_string(),
|
|
servers: u
|
|
.servers
|
|
.iter()
|
|
.map(|s| UpstreamServerData {
|
|
address: s.address.clone(),
|
|
weight: s.weight,
|
|
backup: s.backup,
|
|
down: s.down,
|
|
})
|
|
.collect(),
|
|
keepalive_connections: u.keepalive_connections,
|
|
})
|
|
.collect();
|
|
|
|
let upstreams_json = serde_json::json!({ "upstreams": upstreams_data });
|
|
let rendered = self
|
|
.handlebars
|
|
.render("upstreams", &upstreams_json)
|
|
.map_err(|e| format!("Template render error: {}", e))?;
|
|
output.push_str(&rendered);
|
|
output.push('\n');
|
|
}
|
|
|
|
// Render virtual hosts
|
|
for vh in &config.virtual_hosts {
|
|
let vh_data = self.convert_virtual_host(vh, config)?;
|
|
let rendered = self
|
|
.handlebars
|
|
.render("default_vhost", &vh_data)
|
|
.map_err(|e| format!("Template render error: {}", e))?;
|
|
output.push_str(&rendered);
|
|
output.push('\n');
|
|
}
|
|
|
|
debug!("Configuration rendered successfully");
|
|
Ok(output)
|
|
}
|
|
|
|
/// Convert proto VirtualHost to template data
|
|
fn convert_virtual_host(
|
|
&self,
|
|
vh: &nxmesh_proto::VirtualHost,
|
|
config: &nxmesh_proto::ConfigUpdate,
|
|
) -> Result<VirtualHostData, Box<dyn std::error::Error + Send + Sync>> {
|
|
// Build certificate paths if SSL is enabled
|
|
let (cert_path, key_path) = if vh.ssl_enabled && !vh.ssl_certificate_id.is_empty() {
|
|
if let Some(cert) = config.certificates.get(&vh.ssl_certificate_id) {
|
|
(
|
|
format!("/etc/nginx/certs/{}.crt", cert.id),
|
|
format!("/etc/nginx/certs/{}.key", cert.id),
|
|
)
|
|
} else {
|
|
(
|
|
format!("/etc/nginx/certs/{}.crt", vh.ssl_certificate_id),
|
|
format!("/etc/nginx/certs/{}.key", vh.ssl_certificate_id),
|
|
)
|
|
}
|
|
} else {
|
|
(String::new(), String::new())
|
|
};
|
|
|
|
// Convert locations
|
|
let locations: Vec<LocationData> = vh
|
|
.locations
|
|
.iter()
|
|
.map(|loc| LocationData {
|
|
path: loc.path.clone(),
|
|
proxy_pass: loc.proxy_pass.clone(),
|
|
upstream_name: loc.upstream_id.clone(),
|
|
root: loc.root.clone(),
|
|
index: loc.index.clone(),
|
|
custom_headers: loc
|
|
.custom_headers
|
|
.iter()
|
|
.map(|h| HeaderData {
|
|
name: h.name.clone(),
|
|
value: h.value.clone(),
|
|
always: h.always,
|
|
})
|
|
.collect(),
|
|
})
|
|
.collect();
|
|
|
|
Ok(VirtualHostData {
|
|
id: vh.id.clone(),
|
|
name: vh.name.clone(),
|
|
server_name: vh.server_name.clone(),
|
|
listen_port: vh.listen_port,
|
|
ssl_enabled: vh.ssl_enabled,
|
|
ssl_certificate_path: cert_path,
|
|
ssl_certificate_key_path: key_path,
|
|
http2_enabled: vh.http2_enabled,
|
|
locations,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Default for ConfigRenderer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Convert algorithm enum to name
|
|
fn algorithm_name(algorithm: i32) -> &'static str {
|
|
match algorithm {
|
|
1 => "least_conn",
|
|
2 => "ip_hash",
|
|
3 => "weighted",
|
|
_ => "round_robin",
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_render_config() {
|
|
let renderer = ConfigRenderer::new();
|
|
|
|
let config = nxmesh_proto::ConfigUpdate {
|
|
config_id: "test".to_string(),
|
|
version: 1,
|
|
virtual_hosts: vec![nxmesh_proto::VirtualHost {
|
|
id: "vh1".to_string(),
|
|
name: "Test Site".to_string(),
|
|
server_name: "example.com".to_string(),
|
|
listen_port: 80,
|
|
ssl_enabled: false,
|
|
ssl_certificate_id: "".to_string(),
|
|
http2_enabled: false,
|
|
http3_enabled: false,
|
|
locations: vec![nxmesh_proto::Location {
|
|
path: "/".to_string(),
|
|
proxy_pass: "http://backend".to_string(),
|
|
upstream_id: "".to_string(),
|
|
root: "".to_string(),
|
|
index: "".to_string(),
|
|
custom_headers: vec![],
|
|
rewrite_rules: vec![],
|
|
custom_directives: Default::default(),
|
|
}],
|
|
custom_directives: Default::default(),
|
|
}],
|
|
upstreams: vec![nxmesh_proto::Upstream {
|
|
id: "up1".to_string(),
|
|
name: "backend".to_string(),
|
|
algorithm: 0,
|
|
servers: vec![nxmesh_proto::UpstreamServer {
|
|
address: "127.0.0.1:8080".to_string(),
|
|
weight: 1,
|
|
backup: false,
|
|
down: false,
|
|
max_fails: 1,
|
|
fail_timeout_seconds: 10,
|
|
}],
|
|
health_check: None,
|
|
keepalive_connections: 0,
|
|
}],
|
|
certificates: Default::default(),
|
|
global_settings: None,
|
|
};
|
|
|
|
let result = renderer.render_config(&config).unwrap();
|
|
assert!(result.contains("upstream backend"));
|
|
assert!(result.contains("server_name example.com"));
|
|
assert!(result.contains("proxy_pass http://backend"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_with_ssl() {
|
|
let renderer = ConfigRenderer::new();
|
|
|
|
let config = nxmesh_proto::ConfigUpdate {
|
|
config_id: "test".to_string(),
|
|
version: 1,
|
|
virtual_hosts: vec![nxmesh_proto::VirtualHost {
|
|
id: "vh1".to_string(),
|
|
name: "Test Site".to_string(),
|
|
server_name: "example.com".to_string(),
|
|
listen_port: 443,
|
|
ssl_enabled: true,
|
|
ssl_certificate_id: "cert-123".to_string(),
|
|
http2_enabled: false,
|
|
http3_enabled: false,
|
|
locations: vec![nxmesh_proto::Location {
|
|
path: "/".to_string(),
|
|
proxy_pass: "".to_string(),
|
|
upstream_id: "".to_string(),
|
|
root: "/var/www".to_string(),
|
|
index: "index.html".to_string(),
|
|
custom_headers: vec![nxmesh_proto::Header {
|
|
name: "X-Frame-Options".to_string(),
|
|
value: "DENY".to_string(),
|
|
always: true,
|
|
}],
|
|
rewrite_rules: vec![],
|
|
custom_directives: Default::default(),
|
|
}],
|
|
custom_directives: Default::default(),
|
|
}],
|
|
upstreams: vec![],
|
|
certificates: std::collections::HashMap::from([(
|
|
"cert-123".to_string(),
|
|
nxmesh_proto::Certificate {
|
|
id: "cert-123".to_string(),
|
|
domain: "example.com".to_string(),
|
|
certificate_pem: "".to_string(),
|
|
private_key_pem: "".to_string(),
|
|
expires_at: 0,
|
|
},
|
|
)]),
|
|
global_settings: None,
|
|
};
|
|
|
|
let result = renderer.render_config(&config).unwrap();
|
|
assert!(result.contains("listen 443 ssl"));
|
|
assert!(result.contains("ssl_certificate"));
|
|
assert!(result.contains("root /var/www"));
|
|
assert!(result.contains("add_header X-Frame-Options"));
|
|
}
|
|
}
|