1use 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#[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#[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#[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#[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#[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#[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#[async_trait]
131pub trait ChaosPlugin: Send + Sync {
132 fn metadata(&self) -> &PluginMetadata;
134
135 fn capabilities(&self) -> Vec<PluginCapability>;
137
138 async fn initialize(&mut self, config: PluginConfig) -> Result<()>;
140
141 async fn execute(&self, context: PluginContext) -> Result<PluginResult>;
143
144 async fn cleanup(&mut self) -> Result<()>;
146
147 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 fn config_schema(&self) -> Option<JsonValue> {
157 None
158 }
159}
160
161#[async_trait]
163pub trait PluginHook: Send + Sync {
164 async fn before_execute(&self, _context: &PluginContext) -> Result<()> {
166 Ok(())
167 }
168
169 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
179pub 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 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 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 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 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 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 pub fn register_hook(&self, hook: Arc<dyn PluginHook>) {
239 let mut hooks = self.hooks.write();
240 hooks.push(hook);
241 }
242
243 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 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 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 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 let hooks = self.hooks.read().clone();
277 for hook in &hooks {
278 hook.before_execute(&context).await?;
279 }
280
281 let result = match plugin.execute(context.clone()).await {
283 Ok(result) => {
284 for hook in &hooks {
286 hook.after_execute(&context, &result).await?;
287 }
288 result
289 }
290 Err(error) => {
291 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 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 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 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
335pub 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 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
427pub 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 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 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}