This commit is contained in:
2025-04-04 18:21:16 +02:00
parent eca7e6f552
commit c9b4010089
18 changed files with 1018 additions and 45 deletions

30
src/bin/herodo.rs Normal file
View File

@@ -0,0 +1,30 @@
//! Herodo binary entry point
//!
//! This is the main entry point for the herodo binary.
//! It parses command line arguments and calls into the implementation in the cmd module.
use clap::{App, Arg};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse command line arguments
let matches = App::new("herodo")
.version("0.1.0")
.author("SAL Team")
.about("Executes Rhai scripts for SAL")
.arg(
Arg::with_name("path")
.short("p")
.long("path")
.value_name("PATH")
.help("Path to directory containing Rhai scripts")
.required(true)
.takes_value(true),
)
.get_matches();
// Get the script path from arguments
let script_path = matches.value_of("path").unwrap();
// Call the run function from the cmd module
sal::cmd::herodo::run(script_path)
}

84
src/cmd/herodo.rs Normal file
View File

@@ -0,0 +1,84 @@
//! Herodo - A Rhai script executor for SAL
//!
//! This binary loads the Rhai engine, registers all SAL modules,
//! and executes Rhai scripts from a specified directory in sorted order.
// Removed unused imports
use rhai::Engine;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
/// Run the herodo script executor with the given script path
///
/// # Arguments
///
/// * `script_path` - Path to the directory containing Rhai scripts
///
/// # Returns
///
/// Result indicating success or failure
pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> {
let script_dir = Path::new(script_path);
// Check if the directory exists
if !script_dir.exists() || !script_dir.is_dir() {
eprintln!("Error: '{}' is not a valid directory", script_path);
process::exit(1);
}
// Create a new Rhai engine
let mut engine = Engine::new();
// Register println function for output
engine.register_fn("println", |s: &str| println!("{}", s));
// Register all SAL modules with the engine
crate::rhai::register(&mut engine)?;
// Find all .rhai files in the directory
let mut script_files: Vec<PathBuf> = fs::read_dir(script_dir)?
.filter_map(Result::ok)
.filter(|entry| {
entry.path().is_file() &&
entry.path().extension().map_or(false, |ext| ext == "rhai")
})
.map(|entry| entry.path())
.collect();
// Sort the script files by name
script_files.sort();
if script_files.is_empty() {
println!("No Rhai scripts found in '{}'", script_path);
return Ok(());
}
println!("Found {} Rhai scripts to execute:", script_files.len());
// Execute each script in sorted order
for script_file in script_files {
println!("\nExecuting: {}", script_file.display());
// Read the script content
let script = fs::read_to_string(&script_file)?;
// Execute the script
match engine.eval::<rhai::Dynamic>(&script) {
Ok(result) => {
println!("Script executed successfully");
if !result.is_unit() {
println!("Result: {}", result);
}
},
Err(err) => {
eprintln!("Error executing script: {}", err);
// Continue with the next script instead of stopping
}
}
}
println!("\nAll scripts executed");
Ok(())
}

5
src/cmd/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
//! Command-line tools for SAL
//!
//! This module contains command-line tools built on top of the SAL library.
pub mod herodo;

View File

@@ -43,6 +43,7 @@ pub mod redisclient;
pub mod text;
pub mod virt;
pub mod rhai;
pub mod cmd;
// Version information
/// Returns the version of the SAL library

391
src/rhai/buildah.rs Normal file
View File

@@ -0,0 +1,391 @@
//! Rhai wrappers for Buildah module functions
//!
//! This module provides Rhai wrappers for the functions in the Buildah module.
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
use std::collections::HashMap;
use crate::virt::buildah::{self, BuildahError, Image};
use crate::process::CommandResult;
/// Register Buildah module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_buildah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register types
register_buildah_types(engine)?;
// Register container functions
engine.register_fn("buildah_from", from);
engine.register_fn("buildah_run", run);
engine.register_fn("buildah_run_with_isolation", run_with_isolation);
engine.register_fn("buildah_copy", copy);
engine.register_fn("buildah_add", add);
engine.register_fn("buildah_commit", commit);
engine.register_fn("buildah_remove", remove);
engine.register_fn("buildah_list", list);
engine.register_fn("buildah_build", build_with_options);
engine.register_fn("buildah_new_build_options", new_build_options);
// Register image functions
engine.register_fn("buildah_images", images);
engine.register_fn("buildah_image_remove", image_remove);
engine.register_fn("buildah_image_push", image_push);
engine.register_fn("buildah_image_tag", image_tag);
engine.register_fn("buildah_image_pull", image_pull);
engine.register_fn("buildah_image_commit", image_commit_with_options);
engine.register_fn("buildah_new_commit_options", new_commit_options);
engine.register_fn("buildah_config", config_with_options);
engine.register_fn("buildah_new_config_options", new_config_options);
Ok(())
}
/// Register Buildah module types with the Rhai engine
fn register_buildah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register Image type and methods
engine.register_type_with_name::<Image>("BuildahImage");
// Register getters for Image properties
engine.register_get("id", |img: &mut Image| img.id.clone());
engine.register_get("names", |img: &mut Image| {
let mut array = Array::new();
for name in &img.names {
array.push(Dynamic::from(name.clone()));
}
array
});
engine.register_get("size", |img: &mut Image| img.size.clone());
engine.register_get("created", |img: &mut Image| img.created.clone());
Ok(())
}
// Helper functions for error conversion
fn buildah_error_to_rhai_error<T>(result: Result<T, BuildahError>) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Buildah error: {}", e).into(),
rhai::Position::NONE
))
})
}
/// Create a new Map with default build options
pub fn new_build_options() -> Map {
let mut map = Map::new();
map.insert("tag".into(), Dynamic::UNIT);
map.insert("context_dir".into(), Dynamic::from("."));
map.insert("file".into(), Dynamic::from("Dockerfile"));
map.insert("isolation".into(), Dynamic::UNIT);
map
}
/// Create a new Map with default commit options
pub fn new_commit_options() -> Map {
let mut map = Map::new();
map.insert("format".into(), Dynamic::UNIT);
map.insert("squash".into(), Dynamic::from(false));
map.insert("rm".into(), Dynamic::from(false));
map
}
/// Create a new Map for config options
pub fn new_config_options() -> Map {
Map::new()
}
//
// Container Function Wrappers
//
/// Wrapper for buildah::from
///
/// Create a container from an image.
pub fn from(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::from(image))
}
/// Wrapper for buildah::run
///
/// Run a command in a container.
pub fn run(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::run(container, command))
}
/// Wrapper for buildah::run_with_isolation
///
/// Run a command in a container with specified isolation.
pub fn run_with_isolation(container: &str, command: &str, isolation: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::run_with_isolation(container, command, isolation))
}
/// Wrapper for buildah::copy
///
/// Copy files into a container.
pub fn copy(container: &str, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::copy(container, source, dest))
}
/// Wrapper for buildah::add
///
/// Add files into a container.
pub fn add(container: &str, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::add(container, source, dest))
}
/// Wrapper for buildah::commit
///
/// Commit a container to an image.
pub fn commit(container: &str, image_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::commit(container, image_name))
}
/// Wrapper for buildah::remove
///
/// Remove a container.
pub fn remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::remove(container))
}
/// Wrapper for buildah::list
///
/// List containers.
pub fn list() -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::list())
}
/// Build an image with options specified in a Map
///
/// This provides a builder-style interface for Rhai scripts.
///
/// # Example
///
/// ```rhai
/// let options = buildah_new_build_options();
/// options.tag = "my-image:latest";
/// options.context_dir = ".";
/// options.file = "Dockerfile";
/// options.isolation = "chroot";
/// let result = buildah_build(options);
/// ```
pub fn build_with_options(options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
// Extract options from the map
let tag_option = match options.get("tag") {
Some(tag) => {
if tag.is_unit() {
None
} else if let Ok(tag_str) = tag.clone().into_string() {
Some(tag_str)
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"tag must be a string".into(),
rhai::Position::NONE
)));
}
},
None => None
};
let context_dir = match options.get("context_dir") {
Some(dir) => {
if let Ok(dir_str) = dir.clone().into_string() {
dir_str
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"context_dir must be a string".into(),
rhai::Position::NONE
)));
}
},
None => String::from(".")
};
let file = match options.get("file") {
Some(file) => {
if let Ok(file_str) = file.clone().into_string() {
file_str
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"file must be a string".into(),
rhai::Position::NONE
)));
}
},
None => String::from("Dockerfile")
};
let isolation_option = match options.get("isolation") {
Some(isolation) => {
if isolation.is_unit() {
None
} else if let Ok(isolation_str) = isolation.clone().into_string() {
Some(isolation_str)
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"isolation must be a string".into(),
rhai::Position::NONE
)));
}
},
None => None
};
// Convert String to &str for the function call
let tag_ref = tag_option.as_deref();
let isolation_ref = isolation_option.as_deref();
// Call the buildah build function
buildah_error_to_rhai_error(buildah::build(tag_ref, &context_dir, &file, isolation_ref))
}
//
// Image Function Wrappers
//
/// Wrapper for buildah::images
///
/// List images in local storage.
pub fn images() -> Result<Array, Box<EvalAltResult>> {
let images = buildah_error_to_rhai_error(buildah::images())?;
// Convert Vec<Image> to Rhai Array
let mut array = Array::new();
for image in images {
array.push(Dynamic::from(image));
}
Ok(array)
}
/// Wrapper for buildah::image_remove
///
/// Remove one or more images.
pub fn image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::image_remove(image))
}
/// Wrapper for buildah::image_push
///
/// Push an image to a registry.
pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::image_push(image, destination, tls_verify))
}
/// Wrapper for buildah::image_tag
///
/// Add an additional name to a local image.
pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::image_tag(image, new_name))
}
/// Wrapper for buildah::image_pull
///
/// Pull an image from a registry.
pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
buildah_error_to_rhai_error(buildah::image_pull(image, tls_verify))
}
/// Commit a container to an image with options specified in a Map
///
/// This provides a builder-style interface for Rhai scripts.
///
/// # Example
///
/// ```rhai
/// let options = buildah_new_commit_options();
/// options.format = "docker";
/// options.squash = true;
/// options.rm = true;
/// let result = buildah_image_commit("my-container", "my-image:latest", options);
/// ```
pub fn image_commit_with_options(container: &str, image_name: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
// Extract options from the map
let format_option = match options.get("format") {
Some(format) => {
if format.is_unit() {
None
} else if let Ok(format_str) = format.clone().into_string() {
Some(format_str)
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"format must be a string".into(),
rhai::Position::NONE
)));
}
},
None => None
};
let squash = match options.get("squash") {
Some(squash) => {
if let Ok(squash_val) = squash.clone().as_bool() {
squash_val
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"squash must be a boolean".into(),
rhai::Position::NONE
)));
}
},
None => false
};
let rm = match options.get("rm") {
Some(rm) => {
if let Ok(rm_val) = rm.clone().as_bool() {
rm_val
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"rm must be a boolean".into(),
rhai::Position::NONE
)));
}
},
None => false
};
// Convert String to &str for the function call
let format_ref = format_option.as_deref();
// Call the buildah image_commit function
buildah_error_to_rhai_error(buildah::image_commit(container, image_name, format_ref, squash, rm))
}
/// Configure a container with options specified in a Map
///
/// This provides a builder-style interface for Rhai scripts.
///
/// # Example
///
/// ```rhai
/// let options = buildah_new_config_options();
/// options.author = "John Doe";
/// options.cmd = "echo Hello";
/// options.entrypoint = "/bin/sh -c";
/// let result = buildah_config("my-container", options);
/// ```
pub fn config_with_options(container: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
// Convert Rhai Map to Rust HashMap
let mut config_options = HashMap::<String, String>::new();
for (key, value) in options.iter() {
if let Ok(value_str) = value.clone().into_string() {
// Convert SmartString to String
config_options.insert(key.to_string(), value_str);
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Option '{}' must be a string", key).into(),
rhai::Position::NONE
)));
}
}
// Call the buildah config function
buildah_error_to_rhai_error(buildah::config(container, config_options))
}

View File

@@ -6,15 +6,49 @@
mod error;
mod os;
mod process;
mod buildah;
#[cfg(test)]
mod tests;
use rhai::Engine;
// Re-export common Rhai types for convenience
pub use rhai::{Array, Dynamic, Map, EvalAltResult, Engine};
// Re-export error module
pub use error::*;
pub use os::*;
pub use process::*;
// Re-export specific functions from modules to avoid name conflicts
pub use os::{
register_os_module,
// File system functions
exist, find_file, find_files, find_dir, find_dirs,
delete, mkdir, file_size, rsync,
// Download functions
download, download_install
};
pub use process::{
register_process_module,
// Run functions
run_command, run_silent, run_with_options, new_run_options,
// Process management functions
which, kill, process_list, process_get
};
pub use buildah::{
register_buildah_module,
// Container functions
from, run, run_with_isolation, add, commit, remove, list,
build_with_options, new_build_options,
// Image functions
images, image_remove, image_push, image_tag, image_pull,
image_commit_with_options, new_commit_options,
config_with_options, new_config_options
};
// Rename copy functions to avoid conflicts
pub use os::copy as os_copy;
pub use buildah::copy as buildah_copy;
/// Register all SAL modules with the Rhai engine
///
@@ -41,6 +75,9 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
// Register Process module functions
process::register_process_module(engine)?;
// Register Buildah module functions
buildah::register_buildah_module(engine)?;
// Future modules can be registered here
Ok(())

View File

@@ -2,7 +2,7 @@
//!
//! This module provides Rhai wrappers for the functions in the Process module.
use rhai::{Engine, EvalAltResult, Array, Dynamic};
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
use crate::process::{self, CommandResult, ProcessInfo, RunError, ProcessError};
/// Register Process module functions with the Rhai engine
@@ -21,6 +21,8 @@ pub fn register_process_module(engine: &mut Engine) -> Result<(), Box<EvalAltRes
// Register run functions
engine.register_fn("run_command", run_command);
engine.register_fn("run_silent", run_silent);
engine.register_fn("run", run_with_options);
engine.register_fn("new_run_options", new_run_options);
// Register process management functions
engine.register_fn("which", which);
@@ -51,9 +53,6 @@ fn register_process_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
engine.register_get("memory", |p: &mut ProcessInfo| p.memory);
engine.register_get("cpu", |p: &mut ProcessInfo| p.cpu);
// Register error conversion functions
engine.register_fn("to_string", |err: &str| err.to_string());
Ok(())
}
@@ -76,6 +75,16 @@ fn process_error_to_rhai_error<T>(result: Result<T, ProcessError>) -> Result<T,
})
}
/// Create a new Map with default run options
pub fn new_run_options() -> Map {
let mut map = Map::new();
map.insert("die".into(), Dynamic::from(true));
map.insert("silent".into(), Dynamic::from(false));
map.insert("async_exec".into(), Dynamic::from(false));
map.insert("log".into(), Dynamic::from(false));
map
}
//
// Run Function Wrappers
//
@@ -94,6 +103,50 @@ pub fn run_silent(command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run_silent(command))
}
/// Run a command with options specified in a Map
///
/// This provides a builder-style interface for Rhai scripts.
///
/// # Example
///
/// ```rhai
/// let options = new_run_options();
/// options.die = false;
/// options.silent = true;
/// let result = run("echo Hello", options);
/// ```
pub fn run_with_options(command: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
let mut builder = process::run(command);
// Apply options from the map
if let Some(die) = options.get("die") {
if let Ok(die_val) = die.clone().as_bool() {
builder = builder.die(die_val);
}
}
if let Some(silent) = options.get("silent") {
if let Ok(silent_val) = silent.clone().as_bool() {
builder = builder.silent(silent_val);
}
}
if let Some(async_exec) = options.get("async_exec") {
if let Ok(async_val) = async_exec.clone().as_bool() {
builder = builder.async_exec(async_val);
}
}
if let Some(log) = options.get("log") {
if let Ok(log_val) = log.clone().as_bool() {
builder = builder.log(log_val);
}
}
// Execute the command
run_error_to_rhai_error(builder.execute())
}
//
// Process Management Function Wrappers
//

View File

@@ -117,6 +117,36 @@ mod tests {
assert_eq!(result, ());
}
#[test]
fn test_run_with_options() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test running a command with custom options
#[cfg(target_os = "windows")]
let script = r#"
let options = new_run_options();
options["die"] = true;
options["silent"] = false;
options["log"] = true;
let result = run("echo Hello World", options);
result.success && result.stdout.contains("Hello World")
"#;
#[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#"
let options = new_run_options();
options["die"] = true;
options["silent"] = false;
options["log"] = true;
let result = run("echo 'Hello World'", options);
result.success && result.stdout.contains("Hello World")
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
#[test]
fn test_run_command() {
let mut engine = Engine::new();