upend/cli/src/plugins/mod.rs

176 lines
5.8 KiB
Rust

use anyhow::Result;
use extism::{manifest::Wasm, *};
use std::{
path::{Path, PathBuf},
process::Command,
};
use upend_extension_base::PluginInfo;
mod host;
pub struct Plugins<'a> {
pub plugins: Vec<UpEndPlugin<'a>>,
}
#[derive(Debug)]
pub enum UpEndPlugin<'a> {
Initialized(UpEndPluginInitialized<'a>),
Failed(UpEndPluginFailed),
}
pub struct UpEndPluginInitialized<'a> {
pub path: PathBuf,
pub info: PluginInfo,
pub plugin: Box<extism::Plugin<'a>>,
pub verified: Result<(), String>,
}
impl std::fmt::Debug for UpEndPluginInitialized<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UpEndPluginInitialized")
.field("path", &self.path)
.field("info", &self.info)
.field("verified", &self.verified)
.finish()
}
}
#[derive(Debug)]
pub struct UpEndPluginFailed {
pub path: PathBuf,
pub error: String,
}
impl Plugins<'_> {
pub fn init(plugin_path: &Path) -> Result<Self> {
let plugin_files = plugin_path
.read_dir()?
.filter_map(|p| p.ok().and_then(|p| Some(p.path())))
.filter(|p| p.is_file() && p.extension().unwrap_or_default() == "wasm");
let mut plugins = vec![];
for plugin_path in plugin_files {
debug!("Attempting to load plugin: {:?}", plugin_path);
let file = Wasm::file(plugin_path.clone());
let manifest = Manifest::new([file]);
let plugin = Plugin::create_with_manifest(&manifest, [], true);
match plugin {
Ok(mut plugin) => {
debug!("Plugin loaded: {:?}", plugin_path);
let info = plugin.call("info", []).and_then(|v| {
serde_json::from_slice::<PluginInfo>(&v).map_err(|e| anyhow::anyhow!(e))
});
match info {
Ok(info) => {
debug!("Plugin info: {:?}", info);
let mut gpg_cmd = Command::new("gpg");
let verify_cmd = gpg_cmd
.arg("--verify")
.arg(&plugin_path.with_extension("wasm.asc"));
let verify_result = verify_cmd
.output()
.map_err(|e| format!("Failed to run gpg: {:?}", e))
.and_then(|output| {
if output.status.success() {
Ok(())
} else {
Err(format!(
"Failed to verify plugin: {:?}",
String::from_utf8_lossy(&output.stderr)
))
}
});
let verified = verify_result.and_then(|_| Ok(()));
plugins.push(UpEndPlugin::Initialized(UpEndPluginInitialized {
path: plugin_path.clone(),
info,
plugin: Box::new(plugin),
verified,
}));
}
Err(e) => {
error!("Failed to get plugin info: {:?}", e);
plugins.push(UpEndPlugin::Failed(UpEndPluginFailed {
path: plugin_path.clone(),
error: format!("Failed to get plugin info: {:?}", e),
}));
}
}
}
Err(e) => {
error!("Failed to create plugin: {:?}", e);
plugins.push(UpEndPlugin::Failed(UpEndPluginFailed {
path: plugin_path.clone(),
error: format!("Failed to create plugin: {:?}", e),
}));
}
}
}
Ok(Self { plugins })
}
}
#[cfg(test)]
mod tests {
use core::panic;
use super::*;
use crate::common::get_resource_path;
#[test]
fn test_plugins_init() {
let plugins = Plugins::init(&get_resource_path("plugins").unwrap()).unwrap();
assert!(plugins.plugins.len() > 0);
for plugin in plugins.plugins {
assert!(
match plugin {
UpEndPlugin::Initialized(_) => true,
_ => false,
},
"{:?}",
plugin
);
}
}
#[test]
fn test_plugins_verify() {
let plugins = Plugins::init(&get_resource_path("plugins").unwrap()).unwrap();
let verified_plugin = plugins
.plugins
.iter()
.find(|p| match p {
UpEndPlugin::Initialized(p) => p.path.to_string_lossy().contains("verified"),
_ => false,
})
.unwrap();
assert!(
match verified_plugin {
UpEndPlugin::Initialized(p) => p.verified.is_ok(),
_ => false,
},
"{:?}",
verified_plugin
);
let unverified_plugin = plugins
.plugins
.iter()
.find(|p| match p {
UpEndPlugin::Initialized(p) => !p.path.to_string_lossy().contains("verified"),
_ => false,
})
.unwrap();
assert!(
match unverified_plugin {
UpEndPlugin::Initialized(p) => p.verified.is_err(),
_ => false,
},
"{:?}",
unverified_plugin
);
}
}