mockforge_chaos/
plugins.rs

1//! Chaos Plugin System
2//!
3//! Extensible plugin system for custom chaos engineering functionality.
4//! Allows users to create and integrate custom chaos scenarios, fault injectors,
5//! and resilience patterns.
6
7use async_trait::async_trait;
8use parking_lot::RwLock;
9use serde::{Deserialize, Serialize};
10use serde_json::Value as JsonValue;
11use std::collections::HashMap;
12use std::sync::Arc;
13use thiserror::Error;
14
15/// Plugin system errors
16#[derive(Error, Debug)]
17pub enum PluginError {
18    #[error("Plugin not found: {0}")]
19    PluginNotFound(String),
20
21    #[error("Plugin already registered: {0}")]
22    PluginAlreadyRegistered(String),
23
24    #[error("Plugin initialization failed: {0}")]
25    InitializationFailed(String),
26
27    #[error("Plugin execution failed: {0}")]
28    ExecutionFailed(String),
29
30    #[error("Invalid plugin configuration: {0}")]
31    InvalidConfig(String),
32
33    #[error("Incompatible plugin version: {0}")]
34    IncompatibleVersion(String),
35
36    #[error("Missing required dependency: {0}")]
37    MissingDependency(String),
38}
39
40pub type Result<T> = std::result::Result<T, PluginError>;
41
42/// Plugin metadata
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct PluginMetadata {
45    pub id: String,
46    pub name: String,
47    pub version: String,
48    pub description: String,
49    pub author: String,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub homepage: Option<String>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub repository: Option<String>,
54    pub tags: Vec<String>,
55    pub dependencies: Vec<String>,
56    #[serde(default)]
57    pub api_version: String,
58}
59
60/// Plugin capability
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62#[serde(rename_all = "snake_case")]
63pub enum PluginCapability {
64    FaultInjection,
65    TrafficShaping,
66    Observability,
67    Resilience,
68    Scenario,
69    Metrics,
70    Custom(String),
71}
72
73/// Plugin configuration
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct PluginConfig {
76    pub enabled: bool,
77    pub config: HashMap<String, JsonValue>,
78}
79
80impl Default for PluginConfig {
81    fn default() -> Self {
82        Self {
83            enabled: true,
84            config: HashMap::new(),
85        }
86    }
87}
88
89/// Plugin execution context
90#[derive(Debug, Clone, Serialize, Deserialize, Default)]
91pub struct PluginContext {
92    pub tenant_id: Option<String>,
93    pub scenario_id: Option<String>,
94    pub execution_id: Option<String>,
95    pub parameters: HashMap<String, JsonValue>,
96    pub metadata: HashMap<String, String>,
97}
98
99/// Plugin execution result
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PluginResult {
102    pub success: bool,
103    pub message: String,
104    pub data: HashMap<String, JsonValue>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub error: Option<String>,
107}
108
109impl PluginResult {
110    pub fn success(message: String, data: HashMap<String, JsonValue>) -> Self {
111        Self {
112            success: true,
113            message,
114            data,
115            error: None,
116        }
117    }
118
119    pub fn failure(message: String, error: String) -> Self {
120        Self {
121            success: false,
122            message,
123            data: HashMap::new(),
124            error: Some(error),
125        }
126    }
127}
128
129/// Chaos plugin trait
130#[async_trait]
131pub trait ChaosPlugin: Send + Sync {
132    /// Get plugin metadata
133    fn metadata(&self) -> &PluginMetadata;
134
135    /// Get plugin capabilities
136    fn capabilities(&self) -> Vec<PluginCapability>;
137
138    /// Initialize plugin with configuration
139    async fn initialize(&mut self, config: PluginConfig) -> Result<()>;
140
141    /// Execute plugin action
142    async fn execute(&self, context: PluginContext) -> Result<PluginResult>;
143
144    /// Cleanup plugin resources
145    async fn cleanup(&mut self) -> Result<()>;
146
147    /// Validate configuration
148    fn validate_config(&self, config: &PluginConfig) -> Result<()> {
149        if !config.enabled {
150            return Err(PluginError::InvalidConfig("Plugin is disabled".to_string()));
151        }
152        Ok(())
153    }
154
155    /// Get configuration schema (JSON Schema)
156    fn config_schema(&self) -> Option<JsonValue> {
157        None
158    }
159}
160
161/// Plugin lifecycle hook
162#[async_trait]
163pub trait PluginHook: Send + Sync {
164    /// Called before plugin execution
165    async fn before_execute(&self, _context: &PluginContext) -> Result<()> {
166        Ok(())
167    }
168
169    /// Called after plugin execution
170    async fn after_execute(&self, _context: &PluginContext, _result: &PluginResult) -> Result<()> {
171        Ok(())
172    }
173
174    async fn on_error(&self, _context: &PluginContext, _error: &PluginError) -> Result<()> {
175        Ok(())
176    }
177}
178
179/// Plugin registry
180pub struct PluginRegistry {
181    plugins: Arc<RwLock<HashMap<String, Arc<dyn ChaosPlugin>>>>,
182    hooks: Arc<RwLock<Vec<Arc<dyn PluginHook>>>>,
183    configs: Arc<RwLock<HashMap<String, PluginConfig>>>,
184}
185
186impl PluginRegistry {
187    /// Create a new plugin registry
188    pub fn new() -> Self {
189        Self {
190            plugins: Arc::new(RwLock::new(HashMap::new())),
191            hooks: Arc::new(RwLock::new(Vec::new())),
192            configs: Arc::new(RwLock::new(HashMap::new())),
193        }
194    }
195
196    /// Register a plugin
197    pub fn register_plugin(&self, plugin: Arc<dyn ChaosPlugin>) -> Result<()> {
198        let plugin_id = plugin.metadata().id.clone();
199
200        let mut plugins = self.plugins.write();
201
202        if plugins.contains_key(&plugin_id) {
203            return Err(PluginError::PluginAlreadyRegistered(plugin_id));
204        }
205
206        plugins.insert(plugin_id, plugin);
207        Ok(())
208    }
209
210    /// Unregister a plugin
211    pub fn unregister_plugin(&self, plugin_id: &str) -> Result<()> {
212        let mut plugins = self.plugins.write();
213
214        plugins
215            .remove(plugin_id)
216            .ok_or_else(|| PluginError::PluginNotFound(plugin_id.to_string()))?;
217
218        Ok(())
219    }
220
221    /// Get a plugin
222    pub fn get_plugin(&self, plugin_id: &str) -> Result<Arc<dyn ChaosPlugin>> {
223        let plugins = self.plugins.read();
224
225        plugins
226            .get(plugin_id)
227            .cloned()
228            .ok_or_else(|| PluginError::PluginNotFound(plugin_id.to_string()))
229    }
230
231    /// List all plugins
232    pub fn list_plugins(&self) -> Vec<PluginMetadata> {
233        let plugins = self.plugins.read();
234        plugins.values().map(|p| p.metadata().clone()).collect()
235    }
236
237    /// Register a hook
238    pub fn register_hook(&self, hook: Arc<dyn PluginHook>) {
239        let mut hooks = self.hooks.write();
240        hooks.push(hook);
241    }
242
243    /// Configure a plugin
244    pub fn configure_plugin(&self, plugin_id: &str, config: PluginConfig) -> Result<()> {
245        let plugin = self.get_plugin(plugin_id)?;
246        plugin.validate_config(&config)?;
247
248        let mut configs = self.configs.write();
249        configs.insert(plugin_id.to_string(), config);
250
251        Ok(())
252    }
253
254    /// Get plugin configuration
255    pub fn get_config(&self, plugin_id: &str) -> Option<PluginConfig> {
256        let configs = self.configs.read();
257        configs.get(plugin_id).cloned()
258    }
259
260    /// Execute a plugin
261    pub async fn execute_plugin(
262        &self,
263        plugin_id: &str,
264        context: PluginContext,
265    ) -> Result<PluginResult> {
266        let plugin = self.get_plugin(plugin_id)?;
267
268        // Check if plugin is enabled
269        if let Some(config) = self.get_config(plugin_id) {
270            if !config.enabled {
271                return Err(PluginError::ExecutionFailed("Plugin is disabled".to_string()));
272            }
273        }
274
275        // Execute before hooks
276        let hooks = self.hooks.read().clone();
277        for hook in &hooks {
278            hook.before_execute(&context).await?;
279        }
280
281        // Execute plugin
282        let result = match plugin.execute(context.clone()).await {
283            Ok(result) => {
284                // Execute after hooks
285                for hook in &hooks {
286                    hook.after_execute(&context, &result).await?;
287                }
288                result
289            }
290            Err(error) => {
291                // Execute error hooks
292                for hook in &hooks {
293                    hook.on_error(&context, &error).await?;
294                }
295                return Err(error);
296            }
297        };
298
299        Ok(result)
300    }
301
302    /// Find plugins by capability
303    pub fn find_by_capability(&self, capability: &PluginCapability) -> Vec<PluginMetadata> {
304        let plugins = self.plugins.read();
305        plugins
306            .values()
307            .filter(|p| p.capabilities().contains(capability))
308            .map(|p| p.metadata().clone())
309            .collect()
310    }
311
312    /// Initialize all plugins
313    pub async fn initialize_all(&self) -> Result<()> {
314        let plugins = self.plugins.write();
315
316        for (plugin_id, _plugin) in plugins.iter() {
317            let _config = self.get_config(plugin_id).unwrap_or_default();
318
319            // Create a mutable reference to the plugin
320            // Note: This requires the plugin to be properly designed for interior mutability
321            // or we need to store plugins differently
322            tracing::info!("Initializing plugin: {}", plugin_id);
323        }
324
325        Ok(())
326    }
327}
328
329impl Default for PluginRegistry {
330    fn default() -> Self {
331        Self::new()
332    }
333}
334
335/// Example: Custom fault injection plugin
336pub struct CustomFaultPlugin {
337    metadata: PluginMetadata,
338    config: Option<PluginConfig>,
339}
340
341impl CustomFaultPlugin {
342    pub fn new() -> Self {
343        Self {
344            metadata: PluginMetadata {
345                id: "custom-fault-injector".to_string(),
346                name: "Custom Fault Injector".to_string(),
347                version: "1.0.0".to_string(),
348                description: "Inject custom faults into applications".to_string(),
349                author: "MockForge Team".to_string(),
350                homepage: Some("https://mockforge.dev/plugins/custom-fault".to_string()),
351                repository: None,
352                tags: vec!["fault".to_string(), "injection".to_string()],
353                dependencies: vec![],
354                api_version: "v1".to_string(),
355            },
356            config: None,
357        }
358    }
359}
360
361#[async_trait]
362impl ChaosPlugin for CustomFaultPlugin {
363    fn metadata(&self) -> &PluginMetadata {
364        &self.metadata
365    }
366
367    fn capabilities(&self) -> Vec<PluginCapability> {
368        vec![PluginCapability::FaultInjection]
369    }
370
371    async fn initialize(&mut self, config: PluginConfig) -> Result<()> {
372        self.validate_config(&config)?;
373        self.config = Some(config);
374        Ok(())
375    }
376
377    async fn execute(&self, context: PluginContext) -> Result<PluginResult> {
378        // Custom fault injection logic here
379        let fault_type = context
380            .parameters
381            .get("fault_type")
382            .and_then(|v| v.as_str())
383            .unwrap_or("generic");
384
385        let mut data = HashMap::new();
386        data.insert("fault_type".to_string(), JsonValue::String(fault_type.to_string()));
387        data.insert("injected_at".to_string(), JsonValue::String(chrono::Utc::now().to_rfc3339()));
388
389        Ok(PluginResult::success(format!("Injected {} fault", fault_type), data))
390    }
391
392    async fn cleanup(&mut self) -> Result<()> {
393        self.config = None;
394        Ok(())
395    }
396
397    fn config_schema(&self) -> Option<JsonValue> {
398        Some(serde_json::json!({
399            "type": "object",
400            "properties": {
401                "enabled": {
402                    "type": "boolean",
403                    "default": true
404                },
405                "config": {
406                    "type": "object",
407                    "properties": {
408                        "fault_probability": {
409                            "type": "number",
410                            "minimum": 0.0,
411                            "maximum": 1.0,
412                            "default": 0.1
413                        }
414                    }
415                }
416            }
417        }))
418    }
419}
420
421impl Default for CustomFaultPlugin {
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427/// Example: Metrics collection plugin
428pub struct MetricsPlugin {
429    metadata: PluginMetadata,
430    config: Option<PluginConfig>,
431    metrics: Arc<RwLock<Vec<HashMap<String, JsonValue>>>>,
432}
433
434impl MetricsPlugin {
435    pub fn new() -> Self {
436        Self {
437            metadata: PluginMetadata {
438                id: "metrics-collector".to_string(),
439                name: "Metrics Collector".to_string(),
440                version: "1.0.0".to_string(),
441                description: "Collect and aggregate chaos metrics".to_string(),
442                author: "MockForge Team".to_string(),
443                homepage: None,
444                repository: None,
445                tags: vec!["metrics".to_string(), "observability".to_string()],
446                dependencies: vec![],
447                api_version: "v1".to_string(),
448            },
449            config: None,
450            metrics: Arc::new(RwLock::new(Vec::new())),
451        }
452    }
453
454    pub fn get_metrics(&self) -> Vec<HashMap<String, JsonValue>> {
455        let metrics = self.metrics.read();
456        metrics.clone()
457    }
458}
459
460#[async_trait]
461impl ChaosPlugin for MetricsPlugin {
462    fn metadata(&self) -> &PluginMetadata {
463        &self.metadata
464    }
465
466    fn capabilities(&self) -> Vec<PluginCapability> {
467        vec![PluginCapability::Metrics, PluginCapability::Observability]
468    }
469
470    async fn initialize(&mut self, config: PluginConfig) -> Result<()> {
471        self.validate_config(&config)?;
472        self.config = Some(config);
473        Ok(())
474    }
475
476    async fn execute(&self, context: PluginContext) -> Result<PluginResult> {
477        // Collect metrics from context
478        let mut metric = HashMap::new();
479        metric.insert("timestamp".to_string(), JsonValue::String(chrono::Utc::now().to_rfc3339()));
480
481        if let Some(tenant_id) = &context.tenant_id {
482            metric.insert("tenant_id".to_string(), JsonValue::String(tenant_id.clone()));
483        }
484
485        if let Some(scenario_id) = &context.scenario_id {
486            metric.insert("scenario_id".to_string(), JsonValue::String(scenario_id.clone()));
487        }
488
489        // Store metric
490        let mut metrics = self.metrics.write();
491        metrics.push(metric.clone());
492
493        Ok(PluginResult::success("Metric collected".to_string(), metric))
494    }
495
496    async fn cleanup(&mut self) -> Result<()> {
497        let mut metrics = self.metrics.write();
498        metrics.clear();
499        self.config = None;
500        Ok(())
501    }
502}
503
504impl Default for MetricsPlugin {
505    fn default() -> Self {
506        Self::new()
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[tokio::test]
515    async fn test_plugin_registration() {
516        let registry = PluginRegistry::new();
517        let plugin = Arc::new(CustomFaultPlugin::new());
518
519        registry.register_plugin(plugin.clone()).unwrap();
520
521        let retrieved = registry.get_plugin("custom-fault-injector").unwrap();
522        assert_eq!(retrieved.metadata().name, "Custom Fault Injector");
523    }
524
525    #[tokio::test]
526    async fn test_plugin_execution() {
527        let registry = PluginRegistry::new();
528        let plugin = Arc::new(CustomFaultPlugin::new());
529
530        registry.register_plugin(plugin).unwrap();
531
532        let config = PluginConfig::default();
533        registry.configure_plugin("custom-fault-injector", config).unwrap();
534
535        let mut context = PluginContext::default();
536        context
537            .parameters
538            .insert("fault_type".to_string(), JsonValue::String("timeout".to_string()));
539
540        let result = registry.execute_plugin("custom-fault-injector", context).await.unwrap();
541        assert!(result.success);
542    }
543
544    #[tokio::test]
545    async fn test_find_by_capability() {
546        let registry = PluginRegistry::new();
547
548        registry.register_plugin(Arc::new(CustomFaultPlugin::new())).unwrap();
549        registry.register_plugin(Arc::new(MetricsPlugin::new())).unwrap();
550
551        let fault_plugins = registry.find_by_capability(&PluginCapability::FaultInjection);
552        assert_eq!(fault_plugins.len(), 1);
553
554        let metrics_plugins = registry.find_by_capability(&PluginCapability::Metrics);
555        assert_eq!(metrics_plugins.len(), 1);
556    }
557
558    #[tokio::test]
559    async fn test_metrics_plugin() {
560        let plugin = Arc::new(MetricsPlugin::new());
561        let registry = PluginRegistry::new();
562
563        registry.register_plugin(plugin.clone()).unwrap();
564        registry.configure_plugin("metrics-collector", PluginConfig::default()).unwrap();
565
566        let context = PluginContext {
567            tenant_id: Some("tenant-1".to_string()),
568            ..Default::default()
569        };
570
571        let result = registry.execute_plugin("metrics-collector", context).await.unwrap();
572        assert!(result.success);
573
574        let metrics = plugin.get_metrics();
575        assert_eq!(metrics.len(), 1);
576    }
577}