use crate::core::plugin_loader::{resolve_and_verify, ResolvedPlugin};
use crate::core::plugin_runtime;
use crate::core::types::PluginStatus;
use std::path::Path;
pub fn parse_plugin_type(resource_type: &str) -> Option<&str> {
resource_type.strip_prefix("plugin:")
}
#[derive(Debug, Clone)]
pub struct PluginDispatchResult {
pub plugin_name: String,
pub operation: String,
pub success: bool,
pub message: String,
pub status: PluginStatus,
}
pub fn resolve_plugin(plugin_dir: &Path, plugin_name: &str) -> Result<ResolvedPlugin, String> {
let resolved = resolve_and_verify(plugin_dir, plugin_name)?;
if resolved.status != PluginStatus::Converged {
return Err(format!(
"plugin '{}' verification failed: {:?}",
plugin_name, resolved.status
));
}
Ok(resolved)
}
pub fn dispatch_check(
plugin_dir: &Path,
plugin_name: &str,
resource_config: &serde_json::Value,
) -> PluginDispatchResult {
dispatch_operation(plugin_dir, plugin_name, "check", resource_config)
}
pub fn dispatch_apply(
plugin_dir: &Path,
plugin_name: &str,
resource_config: &serde_json::Value,
) -> PluginDispatchResult {
dispatch_operation(plugin_dir, plugin_name, "apply", resource_config)
}
pub fn dispatch_destroy(
plugin_dir: &Path,
plugin_name: &str,
resource_config: &serde_json::Value,
) -> PluginDispatchResult {
dispatch_operation(plugin_dir, plugin_name, "destroy", resource_config)
}
fn dispatch_operation(
plugin_dir: &Path,
plugin_name: &str,
operation: &str,
resource_config: &serde_json::Value,
) -> PluginDispatchResult {
let resolved = match resolve_plugin(plugin_dir, plugin_name) {
Ok(r) => r,
Err(e) => {
return PluginDispatchResult {
plugin_name: plugin_name.into(),
operation: operation.into(),
success: false,
message: e,
status: PluginStatus::Error,
};
}
};
let input_json = serde_json::to_vec(resource_config).unwrap_or_default();
match plugin_runtime::execute_wasm(&resolved.wasm_path, operation, &input_json) {
Ok(result) => PluginDispatchResult {
plugin_name: resolved.manifest.name,
operation: operation.into(),
success: result.success,
message: if result.output.is_empty() {
format!(
"plugin '{}' v{} {operation}",
plugin_name, resolved.manifest.version
)
} else {
result.output
},
status: result.status,
},
Err(e) => PluginDispatchResult {
plugin_name: resolved.manifest.name,
operation: operation.into(),
success: false,
message: format!("wasm exec: {e}"),
status: PluginStatus::Error,
},
}
}
pub fn is_plugin_type(resource_type: &str) -> bool {
resource_type.starts_with("plugin:")
}
pub fn available_plugin_types(plugin_dir: &Path) -> Vec<String> {
crate::core::plugin_loader::list_plugins(plugin_dir)
.into_iter()
.map(|name| format!("plugin:{name}"))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_plugin_type_valid() {
assert_eq!(parse_plugin_type("plugin:nginx"), Some("nginx"));
assert_eq!(parse_plugin_type("plugin:my-custom"), Some("my-custom"));
}
#[test]
fn parse_plugin_type_invalid() {
assert_eq!(parse_plugin_type("package"), None);
assert_eq!(parse_plugin_type("file"), None);
assert_eq!(parse_plugin_type("plugin"), None);
}
#[test]
fn is_plugin_type_check() {
assert!(is_plugin_type("plugin:foo"));
assert!(!is_plugin_type("package"));
assert!(!is_plugin_type("file"));
}
#[test]
fn available_plugin_types_empty() {
let dir = tempfile::tempdir().unwrap();
let types = available_plugin_types(dir.path());
assert!(types.is_empty());
}
#[test]
fn dispatch_check_missing_plugin() {
let dir = tempfile::tempdir().unwrap();
let config = serde_json::json!({"key": "value"});
let result = dispatch_check(dir.path(), "nonexistent", &config);
assert!(!result.success);
assert_eq!(result.operation, "check");
}
#[test]
fn dispatch_apply_missing_plugin() {
let dir = tempfile::tempdir().unwrap();
let config = serde_json::json!({});
let result = dispatch_apply(dir.path(), "nonexistent", &config);
assert!(!result.success);
assert_eq!(result.operation, "apply");
}
#[test]
fn dispatch_destroy_missing_plugin() {
let dir = tempfile::tempdir().unwrap();
let config = serde_json::json!({});
let result = dispatch_destroy(dir.path(), "nonexistent", &config);
assert!(!result.success);
assert_eq!(result.operation, "destroy");
}
#[test]
fn dispatch_check_with_real_plugin() {
let dir = tempfile::tempdir().unwrap();
let plugin_dir = dir.path().join("test-plugin");
std::fs::create_dir_all(&plugin_dir).unwrap();
let wasm_bytes = b"fake wasm module content";
let hash = blake3::hash(wasm_bytes).to_hex().to_string();
std::fs::write(plugin_dir.join("plugin.wasm"), wasm_bytes).unwrap();
std::fs::write(
plugin_dir.join("plugin.yaml"),
format!(
r#"
name: test-plugin
version: "0.1.0"
abi_version: 1
wasm: plugin.wasm
blake3: {hash}
permissions:
fs: {{}}
net: {{}}
env: {{}}
exec: {{}}
"#
),
)
.unwrap();
let config = serde_json::json!({"key": "value"});
let result = dispatch_check(dir.path(), "test-plugin", &config);
assert_eq!(result.operation, "check");
if plugin_runtime::is_runtime_available() {
assert!(!result.success);
assert!(result.message.contains("wasm"));
} else {
assert!(result.success, "dispatch failed: {}", result.message);
}
}
#[test]
fn resolve_plugin_verified() {
let dir = tempfile::tempdir().unwrap();
let plugin_dir = dir.path().join("verified");
std::fs::create_dir_all(&plugin_dir).unwrap();
let wasm_bytes = b"valid wasm bytes";
let hash = blake3::hash(wasm_bytes).to_hex().to_string();
std::fs::write(plugin_dir.join("plugin.wasm"), wasm_bytes).unwrap();
std::fs::write(
plugin_dir.join("plugin.yaml"),
format!(
r#"
name: verified
version: "1.0.0"
abi_version: 1
wasm: plugin.wasm
blake3: {hash}
permissions:
fs: {{}}
net: {{}}
env: {{}}
exec: {{}}
"#
),
)
.unwrap();
let resolved = resolve_plugin(dir.path(), "verified");
assert!(resolved.is_ok());
}
}