use async_trait::async_trait;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PluginError {
#[error("Plugin not found: {0}")]
PluginNotFound(String),
#[error("Plugin already registered: {0}")]
PluginAlreadyRegistered(String),
#[error("Plugin initialization failed: {0}")]
InitializationFailed(String),
#[error("Plugin execution failed: {0}")]
ExecutionFailed(String),
#[error("Invalid plugin configuration: {0}")]
InvalidConfig(String),
#[error("Incompatible plugin version: {0}")]
IncompatibleVersion(String),
#[error("Missing required dependency: {0}")]
MissingDependency(String),
}
pub type Result<T> = std::result::Result<T, PluginError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginMetadata {
pub id: String,
pub name: String,
pub version: String,
pub description: String,
pub author: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
pub tags: Vec<String>,
pub dependencies: Vec<String>,
#[serde(default)]
pub api_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PluginCapability {
FaultInjection,
TrafficShaping,
Observability,
Resilience,
Scenario,
Metrics,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfig {
pub enabled: bool,
pub config: HashMap<String, JsonValue>,
}
impl Default for PluginConfig {
fn default() -> Self {
Self {
enabled: true,
config: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginContext {
pub tenant_id: Option<String>,
pub scenario_id: Option<String>,
pub execution_id: Option<String>,
pub parameters: HashMap<String, JsonValue>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginResult {
pub success: bool,
pub message: String,
pub data: HashMap<String, JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl PluginResult {
pub fn success(message: String, data: HashMap<String, JsonValue>) -> Self {
Self {
success: true,
message,
data,
error: None,
}
}
pub fn failure(message: String, error: String) -> Self {
Self {
success: false,
message,
data: HashMap::new(),
error: Some(error),
}
}
}
#[async_trait]
pub trait ChaosPlugin: Send + Sync {
fn metadata(&self) -> &PluginMetadata;
fn capabilities(&self) -> Vec<PluginCapability>;
async fn initialize(&mut self, config: PluginConfig) -> Result<()>;
async fn execute(&self, context: PluginContext) -> Result<PluginResult>;
async fn cleanup(&mut self) -> Result<()>;
fn validate_config(&self, config: &PluginConfig) -> Result<()> {
if !config.enabled {
return Err(PluginError::InvalidConfig("Plugin is disabled".to_string()));
}
Ok(())
}
fn config_schema(&self) -> Option<JsonValue> {
None
}
}
#[async_trait]
pub trait PluginHook: Send + Sync {
async fn before_execute(&self, _context: &PluginContext) -> Result<()> {
Ok(())
}
async fn after_execute(&self, _context: &PluginContext, _result: &PluginResult) -> Result<()> {
Ok(())
}
async fn on_error(&self, _context: &PluginContext, _error: &PluginError) -> Result<()> {
Ok(())
}
}
pub struct PluginRegistry {
plugins: Arc<RwLock<HashMap<String, Arc<dyn ChaosPlugin>>>>,
hooks: Arc<RwLock<Vec<Arc<dyn PluginHook>>>>,
configs: Arc<RwLock<HashMap<String, PluginConfig>>>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: Arc::new(RwLock::new(HashMap::new())),
hooks: Arc::new(RwLock::new(Vec::new())),
configs: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn register_plugin(&self, plugin: Arc<dyn ChaosPlugin>) -> Result<()> {
let plugin_id = plugin.metadata().id.clone();
let mut plugins = self.plugins.write();
if plugins.contains_key(&plugin_id) {
return Err(PluginError::PluginAlreadyRegistered(plugin_id));
}
plugins.insert(plugin_id, plugin);
Ok(())
}
pub fn unregister_plugin(&self, plugin_id: &str) -> Result<()> {
let mut plugins = self.plugins.write();
plugins
.remove(plugin_id)
.ok_or_else(|| PluginError::PluginNotFound(plugin_id.to_string()))?;
Ok(())
}
pub fn get_plugin(&self, plugin_id: &str) -> Result<Arc<dyn ChaosPlugin>> {
let plugins = self.plugins.read();
plugins
.get(plugin_id)
.cloned()
.ok_or_else(|| PluginError::PluginNotFound(plugin_id.to_string()))
}
pub fn list_plugins(&self) -> Vec<PluginMetadata> {
let plugins = self.plugins.read();
plugins.values().map(|p| p.metadata().clone()).collect()
}
pub fn register_hook(&self, hook: Arc<dyn PluginHook>) {
let mut hooks = self.hooks.write();
hooks.push(hook);
}
pub fn configure_plugin(&self, plugin_id: &str, config: PluginConfig) -> Result<()> {
let plugin = self.get_plugin(plugin_id)?;
plugin.validate_config(&config)?;
let mut configs = self.configs.write();
configs.insert(plugin_id.to_string(), config);
Ok(())
}
pub fn get_config(&self, plugin_id: &str) -> Option<PluginConfig> {
let configs = self.configs.read();
configs.get(plugin_id).cloned()
}
pub async fn execute_plugin(
&self,
plugin_id: &str,
context: PluginContext,
) -> Result<PluginResult> {
let plugin = self.get_plugin(plugin_id)?;
if let Some(config) = self.get_config(plugin_id) {
if !config.enabled {
return Err(PluginError::ExecutionFailed("Plugin is disabled".to_string()));
}
}
let hooks = self.hooks.read().clone();
for hook in &hooks {
hook.before_execute(&context).await?;
}
let result = match plugin.execute(context.clone()).await {
Ok(result) => {
for hook in &hooks {
hook.after_execute(&context, &result).await?;
}
result
}
Err(error) => {
for hook in &hooks {
hook.on_error(&context, &error).await?;
}
return Err(error);
}
};
Ok(result)
}
pub fn find_by_capability(&self, capability: &PluginCapability) -> Vec<PluginMetadata> {
let plugins = self.plugins.read();
plugins
.values()
.filter(|p| p.capabilities().contains(capability))
.map(|p| p.metadata().clone())
.collect()
}
pub async fn initialize_all(&self) -> Result<()> {
let plugins = self.plugins.write();
for (plugin_id, _plugin) in plugins.iter() {
let _config = self.get_config(plugin_id).unwrap_or_default();
tracing::info!("Initializing plugin: {}", plugin_id);
}
Ok(())
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct CustomFaultPlugin {
metadata: PluginMetadata,
config: Option<PluginConfig>,
}
impl CustomFaultPlugin {
pub fn new() -> Self {
Self {
metadata: PluginMetadata {
id: "custom-fault-injector".to_string(),
name: "Custom Fault Injector".to_string(),
version: "1.0.0".to_string(),
description: "Inject custom faults into applications".to_string(),
author: "MockForge Team".to_string(),
homepage: Some("https://mockforge.dev/plugins/custom-fault".to_string()),
repository: None,
tags: vec!["fault".to_string(), "injection".to_string()],
dependencies: vec![],
api_version: "v1".to_string(),
},
config: None,
}
}
}
#[async_trait]
impl ChaosPlugin for CustomFaultPlugin {
fn metadata(&self) -> &PluginMetadata {
&self.metadata
}
fn capabilities(&self) -> Vec<PluginCapability> {
vec![PluginCapability::FaultInjection]
}
async fn initialize(&mut self, config: PluginConfig) -> Result<()> {
self.validate_config(&config)?;
self.config = Some(config);
Ok(())
}
async fn execute(&self, context: PluginContext) -> Result<PluginResult> {
let fault_type = context
.parameters
.get("fault_type")
.and_then(|v| v.as_str())
.unwrap_or("generic");
let mut data = HashMap::new();
data.insert("fault_type".to_string(), JsonValue::String(fault_type.to_string()));
data.insert("injected_at".to_string(), JsonValue::String(chrono::Utc::now().to_rfc3339()));
Ok(PluginResult::success(format!("Injected {} fault", fault_type), data))
}
async fn cleanup(&mut self) -> Result<()> {
self.config = None;
Ok(())
}
fn config_schema(&self) -> Option<JsonValue> {
Some(serde_json::json!({
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
},
"config": {
"type": "object",
"properties": {
"fault_probability": {
"type": "number",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.1
}
}
}
}
}))
}
}
impl Default for CustomFaultPlugin {
fn default() -> Self {
Self::new()
}
}
pub struct MetricsPlugin {
metadata: PluginMetadata,
config: Option<PluginConfig>,
metrics: Arc<RwLock<Vec<HashMap<String, JsonValue>>>>,
}
impl MetricsPlugin {
pub fn new() -> Self {
Self {
metadata: PluginMetadata {
id: "metrics-collector".to_string(),
name: "Metrics Collector".to_string(),
version: "1.0.0".to_string(),
description: "Collect and aggregate chaos metrics".to_string(),
author: "MockForge Team".to_string(),
homepage: None,
repository: None,
tags: vec!["metrics".to_string(), "observability".to_string()],
dependencies: vec![],
api_version: "v1".to_string(),
},
config: None,
metrics: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn get_metrics(&self) -> Vec<HashMap<String, JsonValue>> {
let metrics = self.metrics.read();
metrics.clone()
}
}
#[async_trait]
impl ChaosPlugin for MetricsPlugin {
fn metadata(&self) -> &PluginMetadata {
&self.metadata
}
fn capabilities(&self) -> Vec<PluginCapability> {
vec![PluginCapability::Metrics, PluginCapability::Observability]
}
async fn initialize(&mut self, config: PluginConfig) -> Result<()> {
self.validate_config(&config)?;
self.config = Some(config);
Ok(())
}
async fn execute(&self, context: PluginContext) -> Result<PluginResult> {
let mut metric = HashMap::new();
metric.insert("timestamp".to_string(), JsonValue::String(chrono::Utc::now().to_rfc3339()));
if let Some(tenant_id) = &context.tenant_id {
metric.insert("tenant_id".to_string(), JsonValue::String(tenant_id.clone()));
}
if let Some(scenario_id) = &context.scenario_id {
metric.insert("scenario_id".to_string(), JsonValue::String(scenario_id.clone()));
}
let mut metrics = self.metrics.write();
metrics.push(metric.clone());
Ok(PluginResult::success("Metric collected".to_string(), metric))
}
async fn cleanup(&mut self) -> Result<()> {
let mut metrics = self.metrics.write();
metrics.clear();
self.config = None;
Ok(())
}
}
impl Default for MetricsPlugin {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_plugin_registration() {
let registry = PluginRegistry::new();
let plugin = Arc::new(CustomFaultPlugin::new());
registry.register_plugin(plugin.clone()).unwrap();
let retrieved = registry.get_plugin("custom-fault-injector").unwrap();
assert_eq!(retrieved.metadata().name, "Custom Fault Injector");
}
#[tokio::test]
async fn test_plugin_execution() {
let registry = PluginRegistry::new();
let plugin = Arc::new(CustomFaultPlugin::new());
registry.register_plugin(plugin).unwrap();
let config = PluginConfig::default();
registry.configure_plugin("custom-fault-injector", config).unwrap();
let mut context = PluginContext::default();
context
.parameters
.insert("fault_type".to_string(), JsonValue::String("timeout".to_string()));
let result = registry.execute_plugin("custom-fault-injector", context).await.unwrap();
assert!(result.success);
}
#[tokio::test]
async fn test_find_by_capability() {
let registry = PluginRegistry::new();
registry.register_plugin(Arc::new(CustomFaultPlugin::new())).unwrap();
registry.register_plugin(Arc::new(MetricsPlugin::new())).unwrap();
let fault_plugins = registry.find_by_capability(&PluginCapability::FaultInjection);
assert_eq!(fault_plugins.len(), 1);
let metrics_plugins = registry.find_by_capability(&PluginCapability::Metrics);
assert_eq!(metrics_plugins.len(), 1);
}
#[tokio::test]
async fn test_metrics_plugin() {
let plugin = Arc::new(MetricsPlugin::new());
let registry = PluginRegistry::new();
registry.register_plugin(plugin.clone()).unwrap();
registry.configure_plugin("metrics-collector", PluginConfig::default()).unwrap();
let context = PluginContext {
tenant_id: Some("tenant-1".to_string()),
..Default::default()
};
let result = registry.execute_plugin("metrics-collector", context).await.unwrap();
assert!(result.success);
let metrics = plugin.get_metrics();
assert_eq!(metrics.len(), 1);
}
}