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:
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user