feat(baker/finalize): Implement plugin manifest support and typed
metadata - Rename env vars WS_* to HBW_* for consistency - Add PluginMetadata struct and PluginType enum for typed plugin info - Support manifest.yml files for external plugin discovery - Add shell/rhai/dylib plugin stubs with proper type signatures - Implement shallow argument merge for plugin runtime arguments
This commit is contained in:
@@ -3,4 +3,4 @@ pub const BOOTSTRAP_SCRIPT_PATH: &str = "/bootstrap.sh";
|
||||
pub const BAKE_SCRIPT_PATH: &str = "bake.sh";
|
||||
pub const PIPELINE_SKIP_ERRORCODE: u8 = 233;
|
||||
pub const TRIVIAL_SCRIPT_NAME: &str = "BAKE_TRIVIAL";
|
||||
pub const BAKER_INTERNAL_CONFIG_PREFIX: &str = "_bakerinternal_";
|
||||
pub const FINALIZE_PLUGIN_MANIFEST: &str = "manifest.yml";
|
||||
|
||||
@@ -116,9 +116,9 @@ impl super::types::Engine {
|
||||
command
|
||||
.current_dir(&ctx.working_dir)
|
||||
.envs(&ctx.env_vars)
|
||||
.env("WS_TASKID", &ctx.task_id)
|
||||
.env("WS_PIPELINE", &ctx.pipeline_name)
|
||||
.env("WS_USERNAME", &ctx.username)
|
||||
.env("HBW_TASKID", &ctx.task_id)
|
||||
.env("HBW_PIPELINE", &ctx.pipeline_name)
|
||||
.env("HBW_USERNAME", &ctx.username)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
use crate::constant::FINALIZE_PLUGIN_MANIFEST;
|
||||
use crate::finalize::RawPluginMap;
|
||||
use crate::finalize::plugin::metadata::{PluginMetadata, PluginType};
|
||||
use crate::{EventSender, ExecutionContext, finalize::error::PluginError};
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod dylib;
|
||||
mod internal;
|
||||
mod metadata;
|
||||
mod rhai;
|
||||
mod shell;
|
||||
|
||||
pub struct FinalizePlugin {
|
||||
metadata: serde_yaml::Value,
|
||||
metadata: PluginMetadata,
|
||||
argument: serde_yaml::Value,
|
||||
pub entry: Box<dyn AsyncPluginFn>,
|
||||
}
|
||||
@@ -14,13 +21,13 @@ pub struct FinalizePlugin {
|
||||
impl FinalizePlugin {
|
||||
pub fn new(entry: Box<dyn AsyncPluginFn>) -> Self {
|
||||
Self {
|
||||
metadata: serde_yaml::Value::Null,
|
||||
argument: serde_yaml::Value::Null,
|
||||
metadata: PluginMetadata::default(),
|
||||
argument: serde_yaml::Value::Mapping(Default::default()),
|
||||
entry,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_metadata(mut self, metadata: serde_yaml::Value) -> Self {
|
||||
pub fn with_metadata(mut self, metadata: PluginMetadata) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
@@ -30,9 +37,25 @@ impl FinalizePlugin {
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn call(&self, ctx: &ExecutionContext, event_tx: &EventSender) -> PluginResult {
|
||||
pub async fn call(
|
||||
&self,
|
||||
runtime_argument: serde_yaml::Value,
|
||||
ctx: &ExecutionContext,
|
||||
event_tx: &EventSender,
|
||||
) -> PluginResult {
|
||||
// shallow merge argument
|
||||
let _argument = match (&self.argument, runtime_argument) {
|
||||
(serde_yaml::Value::Mapping(base), serde_yaml::Value::Mapping(runtime)) => {
|
||||
let mut merged = base.clone();
|
||||
for (k, v) in runtime {
|
||||
merged.insert(k.clone(), v.clone());
|
||||
}
|
||||
serde_yaml::Value::Mapping(merged)
|
||||
}
|
||||
_ => self.argument.clone(), // If not both are mappings, just use the original argument
|
||||
};
|
||||
self.entry
|
||||
.call(self.metadata.clone(), self.argument.clone(), ctx, event_tx)
|
||||
.call(&self.metadata, _argument, ctx, event_tx)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -44,13 +67,116 @@ type PluginResult = Result<(), PluginError>;
|
||||
pub trait AsyncPluginFn: Send + Sync {
|
||||
async fn call(
|
||||
&self,
|
||||
metadata: serde_yaml::Value,
|
||||
metadata: &PluginMetadata,
|
||||
argument: serde_yaml::Value,
|
||||
ctx: &ExecutionContext,
|
||||
event_tx: &EventSender,
|
||||
) -> PluginResult;
|
||||
}
|
||||
|
||||
/// Reads and parses a plugin's manifest file.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Optional filesystem path to the plugin directory
|
||||
/// * `name` - Name of the plugin (used for error reporting)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns parsed [`PluginMetadata`] on success, or a [`PluginError`] if the manifest
|
||||
/// cannot be read or parsed.
|
||||
fn read_manifest(path: Option<PathBuf>, name: &str) -> Result<PluginMetadata, PluginError> {
|
||||
let path = path.ok_or_else(|| PluginError::PluginUnavailable {
|
||||
name: name.to_string(),
|
||||
reason: "Plugin not present in filesystem.".to_string(),
|
||||
})?;
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(path.join(FINALIZE_PLUGIN_MANIFEST)).map_err(|e| {
|
||||
PluginError::PluginUnavailable {
|
||||
name: name.to_string(),
|
||||
reason: format!("Failed to read plugin manifest: {}", e),
|
||||
}
|
||||
})?;
|
||||
let mut metadata: PluginMetadata = serde_yaml::from_str(manifest_content.as_str()).map_err(|e| PluginError::PluginUnavailable {
|
||||
name: name.to_string(),
|
||||
reason: format!("Failed to parse plugin manifest: {}", e),
|
||||
})?;
|
||||
metadata.fspath = path;
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Determines the plugin type based on the manifest type or file extension.
|
||||
///
|
||||
/// The function first returns `original` if it's not [`PluginType::Unknown`].
|
||||
/// Otherwise, it infers the type from the entrypoint file extension:
|
||||
/// - `.sh` / `.bash` → [`PluginType::Shell`]
|
||||
/// - `.rhai` → [`PluginType::Rhai`]
|
||||
/// - `.so` / `.dll` / `.dylib` → [`PluginType::Dylib`]
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `original` - Plugin type as specified in the manifest
|
||||
/// * `entrypoint` - Path or filename of the plugin entrypoint
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The inferred [`PluginType`].
|
||||
fn get_plugin_type(original: PluginType, entrypoint: &str) -> PluginType {
|
||||
if original != PluginType::Unknown {
|
||||
return original;
|
||||
}
|
||||
let _suffix = entrypoint.rsplit('.').next().unwrap_or("");
|
||||
match _suffix {
|
||||
"sh" | "bash" => PluginType::Shell,
|
||||
"rhai" => PluginType::Rhai,
|
||||
"so" | "dll" | "dylib" => PluginType::Dylib,
|
||||
_ => PluginType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a plugin entry function instance based on the plugin type.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `plugin_type` - The type of plugin to instantiate
|
||||
/// * `name` - Name of the plugin (used for error reporting)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a boxed [`AsyncPluginFn`] on success, or a [`PluginError`] if the plugin type
|
||||
/// is [`PluginType::Unknown`] and cannot be inferred.
|
||||
fn get_entry(plugin_type: PluginType, name: &str) -> Result<Box<dyn AsyncPluginFn>, PluginError> {
|
||||
match plugin_type {
|
||||
PluginType::Shell => Ok(Box::new(shell::Shell)),
|
||||
PluginType::Rhai => Ok(Box::new(rhai::Rhai)),
|
||||
PluginType::Dylib => Ok(Box::new(dylib::Dylib)),
|
||||
PluginType::Unknown => {
|
||||
log::error!(
|
||||
"Plugin {} has unknown plugin type and cannot be inferred from entrypoint.",
|
||||
name
|
||||
);
|
||||
return Err(PluginError::PluginUnavailable {
|
||||
name: name.to_string(),
|
||||
reason: "Unknown plugin type and cannot be inferred from entrypoint".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers all available plugins (internal and external) into a plugin map.
|
||||
///
|
||||
/// Internal plugins are registered first, followed by external plugins loaded from
|
||||
/// the provided [`RawPluginMap`]. Plugins that are unavailable (e.g., missing runtime)
|
||||
/// are logged and skipped.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `external_plugins` - Map of external plugin configurations
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a [`PluginMap`] containing all registered plugins, or a [`PluginError`] if
|
||||
/// a plugin manifest cannot be read or parsed.
|
||||
pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError> {
|
||||
let mut plugins: PluginMap = HashMap::new();
|
||||
|
||||
@@ -72,12 +198,13 @@ pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError
|
||||
log::warn!("Plugin {} is not available: {}", name, reason);
|
||||
continue;
|
||||
}
|
||||
let metadata = read_manifest(config.runtime.fspath, &name)?;
|
||||
let plugin_type = get_plugin_type(metadata.plugin_type.clone(), &metadata.entrypoint);
|
||||
let entry = get_entry(plugin_type, &name)?;
|
||||
plugins.insert(
|
||||
name,
|
||||
// TODO: Implement function for Shell & Rhai
|
||||
// Use dummy function to please compiler
|
||||
FinalizePlugin::new(Box::new(internal::dummy::Dummy))
|
||||
.with_metadata(config.runtime.manifest.unwrap_or_default())
|
||||
FinalizePlugin::new(entry)
|
||||
.with_metadata(metadata)
|
||||
.with_argument(config.config),
|
||||
);
|
||||
}
|
||||
@@ -85,12 +212,13 @@ pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError
|
||||
Ok(plugins)
|
||||
}
|
||||
|
||||
/// Calls a registered plugin by name, passing the execution context and event sender.
|
||||
/// Calls a registered plugin by name, passing the configuration, execution context and event sender.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `plugins` - Map of registered plugin names to their [`FinalizePlugin`] instances
|
||||
/// * `name` - Name of the plugin to invoke
|
||||
/// * `config` - Configuration value passed to the plugin
|
||||
/// * `ctx` - Execution context for the plugin
|
||||
/// * `event_tx` - Event sender for plugin lifecycle events
|
||||
///
|
||||
@@ -108,6 +236,5 @@ pub async fn call_named_plugin(
|
||||
let plugin = plugins
|
||||
.get(&name)
|
||||
.ok_or_else(|| PluginError::UnknownPlugin(name))?;
|
||||
// TODO: Shallow argument merge
|
||||
plugin.with_argument(config).call(ctx, event_tx).await
|
||||
plugin.call(config, ctx, event_tx).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::finalize::plugin::PluginError;
|
||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub struct Dylib;
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncPluginFn for Dylib {
|
||||
async fn call(
|
||||
&self,
|
||||
metadata: &PluginMetadata,
|
||||
argument: serde_yaml::Value,
|
||||
ctx: &ExecutionContext,
|
||||
event_tx: &EventSender,
|
||||
) -> Result<(), PluginError> {
|
||||
unimplemented!("Dylib plugin is not implemented yet");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::finalize::plugin::PluginError;
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -8,7 +9,7 @@ pub struct Dummy;
|
||||
impl AsyncPluginFn for Dummy {
|
||||
async fn call(
|
||||
&self,
|
||||
_metadata: serde_yaml::Value,
|
||||
_metadata: &PluginMetadata,
|
||||
_argument: serde_yaml::Value,
|
||||
_ctx: &ExecutionContext,
|
||||
_event_tx: &EventSender,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::{EventSender, ExecutionContext, finalize::{error::PluginError, plugin::AsyncPluginFn}};
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -7,7 +8,7 @@ pub struct InsiteNotify;
|
||||
impl AsyncPluginFn for InsiteNotify {
|
||||
async fn call(
|
||||
&self,
|
||||
_metadata: serde_yaml::Value,
|
||||
_metadata: &PluginMetadata,
|
||||
_argument: serde_yaml::Value,
|
||||
_ctx: &ExecutionContext,
|
||||
_event_tx: &EventSender,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::finalize::plugin::PluginError;
|
||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||
use async_trait::async_trait;
|
||||
@@ -8,7 +9,7 @@ pub struct Mail;
|
||||
impl AsyncPluginFn for Mail {
|
||||
async fn call(
|
||||
&self,
|
||||
_metadata: serde_yaml::Value,
|
||||
_metadata: &PluginMetadata,
|
||||
_argument: serde_yaml::Value,
|
||||
_ctx: &ExecutionContext,
|
||||
_event_tx: &EventSender,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::finalize::plugin::PluginError;
|
||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||
use async_trait::async_trait;
|
||||
@@ -8,7 +9,7 @@ pub struct Satori;
|
||||
impl AsyncPluginFn for Satori {
|
||||
async fn call(
|
||||
&self,
|
||||
_metadata: serde_yaml::Value,
|
||||
_metadata: &PluginMetadata,
|
||||
_argument: serde_yaml::Value,
|
||||
_ctx: &ExecutionContext,
|
||||
_event_tx: &EventSender,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::finalize::plugin::PluginError;
|
||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||
use async_trait::async_trait;
|
||||
@@ -8,7 +9,7 @@ pub struct Webhook;
|
||||
impl AsyncPluginFn for Webhook {
|
||||
async fn call(
|
||||
&self,
|
||||
_metadata: serde_yaml::Value,
|
||||
_metadata: &PluginMetadata,
|
||||
_argument: serde_yaml::Value,
|
||||
_ctx: &ExecutionContext,
|
||||
_event_tx: &EventSender,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use std::path::PathBuf;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PluginMetadata {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
|
||||
pub entrypoint: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub plugin_type: PluginType,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub value: serde_yaml::Value,
|
||||
|
||||
#[serde(skip)]
|
||||
pub fspath: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for PluginMetadata {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::new(),
|
||||
version: String::new(),
|
||||
entrypoint: String::new(),
|
||||
plugin_type: PluginType::Unknown,
|
||||
value: serde_yaml::Value::Null,
|
||||
fspath: PathBuf::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, PartialEq, Eq, Clone)]
|
||||
pub enum PluginType {
|
||||
#[default]
|
||||
Unknown,
|
||||
Shell,
|
||||
Rhai,
|
||||
Dylib,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::finalize::plugin::PluginError;
|
||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub struct Rhai;
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncPluginFn for Rhai {
|
||||
async fn call(
|
||||
&self,
|
||||
_metadata: &PluginMetadata,
|
||||
_argument: serde_yaml::Value,
|
||||
_ctx: &ExecutionContext,
|
||||
_event_tx: &EventSender,
|
||||
) -> Result<(), PluginError> {
|
||||
todo!("Rhai plugin is not implemented yet");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::finalize::plugin::PluginError;
|
||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub struct Shell;
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncPluginFn for Shell {
|
||||
async fn call(
|
||||
&self,
|
||||
metadata: &PluginMetadata,
|
||||
argument: serde_yaml::Value,
|
||||
ctx: &ExecutionContext,
|
||||
event_tx: &EventSender,
|
||||
) -> Result<(), PluginError> {
|
||||
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user