feat: Implement system health metrics collection and reporting

- 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.
This commit is contained in:
GW_MC
2026-03-03 08:51:31 +00:00
parent 4eddf7e094
commit 08b28a2acf
11 changed files with 1619 additions and 128 deletions

View File

@@ -1,45 +1,210 @@
//! Nginx configuration renderer
//! Nginx configuration renderer using Handlebars templates
use handlebars::Handlebars;
use serde_json::json;
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
Self::register_templates(&mut handlebars);
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 }
}
/// Register built-in templates
fn register_templates(handlebars: &mut Handlebars) {
// Default reverse proxy template
handlebars.register_template_string("default", include_str!("templates/default.hbs")).ok();
/// 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)
}
/// Render configuration
pub fn render(&self, template_name: &str, data: &serde_json::Value) -> Result<String, Box<dyn std::error::Error>> {
let rendered = self.handlebars.render(template_name, data)?;
Ok(rendered)
}
/// 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())
};
/// Render virtual host
pub fn render_virtual_host(&self, vh: &nxmesh_core::models::VirtualHost) -> Result<String, Box<dyn std::error::Error>> {
let data = json!({
"server_name": vh.server_name,
"listen_port": vh.listen_port,
"ssl_enabled": vh.ssl_enabled,
"locations": vh.locations,
});
self.render("default", &data)
// 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,
})
}
}
@@ -48,3 +213,124 @@ impl Default for ConfigRenderer {
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"));
}
}