diff --git a/buildah_builder_implementation_plan.md b/buildah_builder_implementation_plan.md new file mode 100644 index 0000000..08f6585 --- /dev/null +++ b/buildah_builder_implementation_plan.md @@ -0,0 +1,752 @@ +# Buildah Builder Implementation Plan + +## Introduction + +This document outlines the plan for changing the buildah interface in the `@src/virt/buildah` module to use a builder object pattern. The current implementation uses standalone functions, which makes the interface less clear and harder to use. The new implementation will use a builder object with methods, which will make the interface more intuitive and easier to use. + +## Current Architecture + +The current buildah implementation has: +- Standalone functions in the buildah module (from, run, images, etc.) +- Functions that operate on container IDs passed as parameters +- No state maintained between function calls +- Rhai wrappers that expose these functions to Rhai scripts + +Example of current usage: +```rust +// Create a container +let result = buildah::from("fedora:latest")?; +let container_id = result.stdout.trim(); + +// Run a command in the container +buildah::run(container_id, "dnf install -y nginx")?; + +// Copy a file into the container +buildah::bah_copy(container_id, "./example.conf", "/etc/example.conf")?; + +// Commit the container to create a new image +buildah::bah_commit(container_id, "my-nginx:latest")?; +``` + +## Proposed Architecture + +We'll change to a builder object pattern where: +- A `Builder` struct is created with a `new()` method that takes a name and image +- All operations (including those not specific to a container) are methods on the Builder +- The Builder maintains state (like container ID) between method calls +- Methods return operation results (CommandResult or other types) for error handling +- Rhai wrappers expose the Builder and its methods to Rhai scripts + +Example of proposed usage: +```rust +// Create a builder +let builder = Builder::new("my-container", "fedora:latest")?; + +// Run a command in the container +builder.run("dnf install -y nginx")?; + +// Copy a file into the container +builder.copy("./example.conf", "/etc/example.conf")?; + +// Commit the container to create a new image +builder.commit("my-nginx:latest")?; +``` + +## Class Diagram + +```mermaid +classDiagram + class Builder { + -String name + -String container_id + -String image + +new(name: String, image: String) -> Result + +run(command: String) -> Result + +run_with_isolation(command: String, isolation: String) -> Result + +add(source: String, dest: String) -> Result + +copy(source: String, dest: String) -> Result + +commit(image_name: String) -> Result + +remove() -> Result + +config(options: HashMap) -> Result + } + + class BuilderStatic { + +images() -> Result, BuildahError> + +image_remove(image: String) -> Result + +image_pull(image: String, tls_verify: bool) -> Result + +image_push(image: String, destination: String, tls_verify: bool) -> Result + +image_tag(image: String, new_name: String) -> Result + +image_commit(container: String, image_name: String, format: Option, squash: bool, rm: bool) -> Result + +build(tag: Option, context_dir: String, file: String, isolation: Option) -> Result + } + + Builder --|> BuilderStatic : Static methods +``` + +## Implementation Plan + +### Step 1: Create the Builder Struct + +1. Create a new file `src/virt/buildah/builder.rs` +2. Define the Builder struct with fields for name, container_id, and image +3. Implement the new() method that creates a container from the image and returns a Builder instance +4. Implement methods for all container operations (run, add, copy, commit, etc.) +5. Implement methods for all image operations (images, image_remove, image_pull, etc.) + +### Step 2: Update the Module Structure + +1. Update `src/virt/buildah/mod.rs` to include the new builder module +2. Re-export the Builder struct and its methods +3. Keep the existing functions for backward compatibility (marked as deprecated) + +### Step 3: Update the Rhai Wrapper + +1. Update `src/rhai/buildah.rs` to register the Builder type with the Rhai engine +2. Register all Builder methods with the Rhai engine +3. Create Rhai-friendly constructor for the Builder +4. Update the existing function wrappers to use the new Builder (or keep them for backward compatibility) + +### Step 4: Update Examples and Tests + +1. Update `examples/buildah.rs` to use the new Builder pattern +2. Update `rhaiexamples/04_buildah_operations.rhai` to use the new Builder pattern +3. Update any tests to use the new Builder pattern + +## Detailed Implementation + +### 1. Builder Struct Definition + +```rust +// src/virt/buildah/builder.rs +pub struct Builder { + name: String, + container_id: Option, + image: String, +} + +impl Builder { + pub fn new(name: &str, image: &str) -> Result { + let result = execute_buildah_command(&["from", "--name", name, image])?; + let container_id = result.stdout.trim().to_string(); + + Ok(Self { + name: name.to_string(), + container_id: Some(container_id), + image: image.to_string(), + }) + } + + // Container methods + pub fn run(&self, command: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["run", container_id, "sh", "-c", command]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + pub fn run_with_isolation(&self, command: &str, isolation: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["run", "--isolation", isolation, container_id, "sh", "-c", command]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + pub fn copy(&self, source: &str, dest: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["copy", container_id, source, dest]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + pub fn add(&self, source: &str, dest: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["add", container_id, source, dest]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + pub fn commit(&self, image_name: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["commit", container_id, image_name]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + pub fn remove(&self) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["rm", container_id]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + pub fn config(&self, options: HashMap) -> Result { + if let Some(container_id) = &self.container_id { + let mut args_owned: Vec = Vec::new(); + args_owned.push("config".to_string()); + + // Process options map + for (key, value) in options.iter() { + let option_name = format!("--{}", key); + args_owned.push(option_name); + args_owned.push(value.clone()); + } + + args_owned.push(container_id.clone()); + + // Convert Vec to Vec<&str> for execute_buildah_command + let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect(); + + execute_buildah_command(&args) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + // Static methods + pub fn images() -> Result, BuildahError> { + // Implementation from current images() function + let result = execute_buildah_command(&["images", "--json"])?; + + // Try to parse the JSON output + match serde_json::from_str::(&result.stdout) { + Ok(json) => { + if let serde_json::Value::Array(images_json) = json { + let mut images = Vec::new(); + + for image_json in images_json { + // Extract image ID + let id = match image_json.get("id").and_then(|v| v.as_str()) { + Some(id) => id.to_string(), + None => return Err(BuildahError::ConversionError("Missing image ID".to_string())), + }; + + // Extract image names + let names = match image_json.get("names").and_then(|v| v.as_array()) { + Some(names_array) => { + let mut names_vec = Vec::new(); + for name_value in names_array { + if let Some(name_str) = name_value.as_str() { + names_vec.push(name_str.to_string()); + } + } + names_vec + }, + None => Vec::new(), // Empty vector if no names found + }; + + // Extract image size + let size = match image_json.get("size").and_then(|v| v.as_str()) { + Some(size) => size.to_string(), + None => "Unknown".to_string(), // Default value if size not found + }; + + // Extract creation timestamp + let created = match image_json.get("created").and_then(|v| v.as_str()) { + Some(created) => created.to_string(), + None => "Unknown".to_string(), // Default value if created not found + }; + + // Create Image struct and add to vector + images.push(Image { + id, + names, + size, + created, + }); + } + + Ok(images) + } else { + Err(BuildahError::JsonParseError("Expected JSON array".to_string())) + } + }, + Err(e) => { + Err(BuildahError::JsonParseError(format!("Failed to parse image list JSON: {}", e))) + } + } + } + + pub fn image_remove(image: &str) -> Result { + execute_buildah_command(&["rmi", image]) + } + + pub fn image_pull(image: &str, tls_verify: bool) -> Result { + let mut args = vec!["pull"]; + + if !tls_verify { + args.push("--tls-verify=false"); + } + + args.push(image); + + execute_buildah_command(&args) + } + + pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result { + let mut args = vec!["push"]; + + if !tls_verify { + args.push("--tls-verify=false"); + } + + args.push(image); + args.push(destination); + + execute_buildah_command(&args) + } + + pub fn image_tag(image: &str, new_name: &str) -> Result { + execute_buildah_command(&["tag", image, new_name]) + } + + pub fn image_commit(container: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result { + let mut args = vec!["commit"]; + + if let Some(format_str) = format { + args.push("--format"); + args.push(format_str); + } + + if squash { + args.push("--squash"); + } + + if rm { + args.push("--rm"); + } + + args.push(container); + args.push(image_name); + + execute_buildah_command(&args) + } + + pub fn build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result { + let mut args = Vec::new(); + args.push("build"); + + if let Some(tag_value) = tag { + args.push("-t"); + args.push(tag_value); + } + + if let Some(isolation_value) = isolation { + args.push("--isolation"); + args.push(isolation_value); + } + + args.push("-f"); + args.push(file); + + args.push(context_dir); + + execute_buildah_command(&args) + } +} +``` + +### 2. Updated Module Structure + +```rust +// src/virt/buildah/mod.rs +mod containers; +mod images; +mod cmd; +mod builder; +#[cfg(test)] +mod containers_test; + +use std::fmt; +use std::error::Error; +use std::io; + +/// Error type for buildah operations +#[derive(Debug)] +pub enum BuildahError { + /// The buildah command failed to execute + CommandExecutionFailed(io::Error), + /// The buildah command executed but returned an error + CommandFailed(String), + /// Failed to parse JSON output + JsonParseError(String), + /// Failed to convert data + ConversionError(String), + /// Generic error + Other(String), +} + +impl fmt::Display for BuildahError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BuildahError::CommandExecutionFailed(e) => write!(f, "Failed to execute buildah command: {}", e), + BuildahError::CommandFailed(e) => write!(f, "Buildah command failed: {}", e), + BuildahError::JsonParseError(e) => write!(f, "Failed to parse JSON: {}", e), + BuildahError::ConversionError(e) => write!(f, "Conversion error: {}", e), + BuildahError::Other(e) => write!(f, "{}", e), + } + } +} + +impl Error for BuildahError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + BuildahError::CommandExecutionFailed(e) => Some(e), + _ => None, + } + } +} + +// Re-export the Builder +pub use builder::Builder; + +// Re-export existing functions for backward compatibility +#[deprecated(since = "0.2.0", note = "Use Builder::new() instead")] +pub use containers::*; +#[deprecated(since = "0.2.0", note = "Use Builder methods instead")] +pub use images::*; +pub use cmd::*; +``` + +### 3. Rhai Wrapper Changes + +```rust +// src/rhai/buildah.rs +//! 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, Builder}; +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>` - Ok if registration was successful, Err otherwise +pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box> { + // Register types + register_bah_types(engine)?; + + // Register Builder constructor + engine.register_fn("bah_new", bah_new); + + // Register Builder instance methods + engine.register_fn("run", |builder: &mut Builder, command: &str| -> Result> { + bah_error_to_rhai_error(builder.run(command)) + }); + + engine.register_fn("run_with_isolation", |builder: &mut Builder, command: &str, isolation: &str| -> Result> { + bah_error_to_rhai_error(builder.run_with_isolation(command, isolation)) + }); + + engine.register_fn("copy", |builder: &mut Builder, source: &str, dest: &str| -> Result> { + bah_error_to_rhai_error(builder.copy(source, dest)) + }); + + engine.register_fn("add", |builder: &mut Builder, source: &str, dest: &str| -> Result> { + bah_error_to_rhai_error(builder.add(source, dest)) + }); + + engine.register_fn("commit", |builder: &mut Builder, image_name: &str| -> Result> { + bah_error_to_rhai_error(builder.commit(image_name)) + }); + + engine.register_fn("remove", |builder: &mut Builder| -> Result> { + bah_error_to_rhai_error(builder.remove()) + }); + + engine.register_fn("config", |builder: &mut Builder, options: Map| -> Result> { + // Convert Rhai Map to Rust HashMap + let config_options = convert_map_to_hashmap(options)?; + bah_error_to_rhai_error(builder.config(config_options)) + }); + + // Register Builder static methods + engine.register_fn("images", |_: &mut Builder| -> Result> { + let images = bah_error_to_rhai_error(Builder::images())?; + + // Convert Vec to Rhai Array + let mut array = Array::new(); + for image in images { + array.push(Dynamic::from(image)); + } + + Ok(array) + }); + + engine.register_fn("image_remove", |_: &mut Builder, image: &str| -> Result> { + bah_error_to_rhai_error(Builder::image_remove(image)) + }); + + engine.register_fn("image_pull", |_: &mut Builder, image: &str, tls_verify: bool| -> Result> { + bah_error_to_rhai_error(Builder::image_pull(image, tls_verify)) + }); + + engine.register_fn("image_push", |_: &mut Builder, image: &str, destination: &str, tls_verify: bool| -> Result> { + bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify)) + }); + + engine.register_fn("image_tag", |_: &mut Builder, image: &str, new_name: &str| -> Result> { + bah_error_to_rhai_error(Builder::image_tag(image, new_name)) + }); + + // Register legacy functions for backward compatibility + register_legacy_functions(engine)?; + + Ok(()) +} + +/// Register Buildah module types with the Rhai engine +fn register_bah_types(engine: &mut Engine) -> Result<(), Box> { + // Register Builder type + engine.register_type_with_name::("BuildahBuilder"); + + // Register getters for Builder properties + engine.register_get("container_id", |builder: &mut Builder| { + match builder.container_id() { + Some(id) => id.clone(), + None => "".to_string(), + } + }); + + engine.register_get("name", |builder: &mut Builder| builder.name().to_string()); + engine.register_get("image", |builder: &mut Builder| builder.image().to_string()); + + // Register Image type and methods (same as before) + engine.register_type_with_name::("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 + }); + // Add a 'name' getter that returns the first name or a default + engine.register_get("name", |img: &mut Image| { + if img.names.is_empty() { + "".to_string() + } else { + img.names[0].clone() + } + }); + engine.register_get("size", |img: &mut Image| img.size.clone()); + engine.register_get("created", |img: &mut Image| img.created.clone()); + + Ok(()) +} + +/// Register legacy functions for backward compatibility +fn register_legacy_functions(engine: &mut Engine) -> Result<(), Box> { + // Register container functions + engine.register_fn("bah_from", bah_from_legacy); + engine.register_fn("bah_run", bah_run_legacy); + engine.register_fn("bah_run_with_isolation", bah_run_with_isolation_legacy); + engine.register_fn("bah_copy", bah_copy_legacy); + engine.register_fn("bah_add", bah_add_legacy); + engine.register_fn("bah_commit", bah_commit_legacy); + engine.register_fn("bah_remove", bah_remove_legacy); + engine.register_fn("bah_list", bah_list_legacy); + engine.register_fn("bah_build", bah_build_with_options_legacy); + engine.register_fn("bah_new_build_options", new_build_options); + + // Register image functions + engine.register_fn("bah_images", images_legacy); + engine.register_fn("bah_image_remove", image_remove_legacy); + engine.register_fn("bah_image_push", image_push_legacy); + engine.register_fn("bah_image_tag", image_tag_legacy); + engine.register_fn("bah_image_pull", image_pull_legacy); + engine.register_fn("bah_image_commit", image_commit_with_options_legacy); + engine.register_fn("bah_new_commit_options", new_commit_options); + engine.register_fn("bah_config", config_with_options_legacy); + engine.register_fn("bah_new_config_options", new_config_options); + + Ok(()) +} + +// Helper functions for error conversion +fn bah_error_to_rhai_error(result: Result) -> Result> { + result.map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Buildah error: {}", e).into(), + rhai::Position::NONE + )) + }) +} + +// Helper function to convert Rhai Map to Rust HashMap +fn convert_map_to_hashmap(options: Map) -> Result, Box> { + let mut config_options = HashMap::::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 + ))); + } + } + + Ok(config_options) +} + +/// Create a new Builder +pub fn bah_new(name: &str, image: &str) -> Result> { + bah_error_to_rhai_error(Builder::new(name, image)) +} + +// Legacy function implementations (for backward compatibility) +// These would call the new Builder methods internally +// ... +``` + +### 4. Example Updates + +#### Rust Example + +```rust +// examples/buildah.rs +//! Example usage of the buildah module +//! +//! This file demonstrates how to use the buildah module to perform +//! common container operations like creating containers, running commands, +//! and managing images. + +use sal::virt::buildah::{Builder, BuildahError}; +use std::collections::HashMap; + +/// Run a complete buildah workflow example +pub fn run_buildah_example() -> Result<(), BuildahError> { + println!("Starting buildah example workflow..."); + + // Step 1: Create a container from an image using the Builder + println!("\n=== Creating container from fedora:latest ==="); + let builder = Builder::new("my-fedora-container", "fedora:latest")?; + println!("Created container: {}", builder.container_id().unwrap_or(&"unknown".to_string())); + + // Step 2: Run a command in the container + println!("\n=== Installing nginx in container ==="); + // Use chroot isolation to avoid BPF issues + let install_result = builder.run_with_isolation("dnf install -y nginx", "chroot")?; + println!("{:#?}", install_result); + println!("Installation output: {}", install_result.stdout); + + // Step 3: Copy a file into the container + println!("\n=== Copying configuration file to container ==="); + builder.copy("./example.conf", "/etc/example.conf")?; + + // Step 4: Configure container metadata + println!("\n=== Configuring container metadata ==="); + let mut config_options = HashMap::new(); + config_options.insert("port".to_string(), "80".to_string()); + config_options.insert("label".to_string(), "maintainer=example@example.com".to_string()); + config_options.insert("entrypoint".to_string(), "/usr/sbin/nginx".to_string()); + builder.config(config_options)?; + println!("Container configured"); + + // Step 5: Commit the container to create a new image + println!("\n=== Committing container to create image ==="); + let image_name = "my-nginx:latest"; + builder.commit(image_name)?; + println!("Created image: {}", image_name); + + // Step 6: List images to verify our new image exists + println!("\n=== Listing images ==="); + let images = Builder::images()?; + println!("Found {} images:", images.len()); + for image in images { + println!(" ID: {}", image.id); + println!(" Names: {}", image.names.join(", ")); + println!(" Size: {}", image.size); + println!(" Created: {}", image.created); + println!(); + } + + // Step 7: Clean up (optional in a real workflow) + println!("\n=== Cleaning up ==="); + Builder::image_remove(image_name)?; + builder.remove()?; + + println!("\nBuildah example workflow completed successfully!"); + Ok(()) +} + +/// Demonstrate how to build an image from a Containerfile/Dockerfile +pub fn build_image_example() -> Result<(), BuildahError> { + println!("Building an image from a Containerfile..."); + + // Use the build function with tag, context directory, and isolation to avoid BPF issues + let result = Builder::build(Some("my-app:latest"), ".", "example_Dockerfile", Some("chroot"))?; + + println!("Build output: {}", result.stdout); + println!("Image built successfully!"); + + Ok(()) +} + +/// Example of pulling and pushing images +pub fn registry_operations_example() -> Result<(), BuildahError> { + println!("Demonstrating registry operations..."); + + // Pull an image + println!("\n=== Pulling an image ==="); + Builder::image_pull("docker.io/library/alpine:latest", true)?; + println!("Image pulled successfully"); + + // Tag the image + println!("\n=== Tagging the image ==="); + Builder::image_tag("alpine:latest", "my-alpine:v1.0")?; + println!("Image tagged successfully"); + + // Push an image (this would typically go to a real registry) + // println!("\n=== Pushing an image (example only) ==="); + // println!("In a real scenario, you would push to a registry with:"); + // println!("Builder::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)"); + + Ok(()) +} + +/// Main function to run all examples +pub fn run_all_examples() -> Result<(), BuildahError> { + println!("=== BUILDAH MODULE EXAMPLES ===\n"); + + run_buildah_example()?; + build_image_example()?; + registry_operations_example()?; + + Ok(()) +} + +fn main() { + let _ = run_all_examples(); +} +``` + +#### Rhai Example + +```rhai +// rhaiexamples/04_buildah_operations.rhai +// Demonstrates container operations using SAL's buildah integration +// Note: This script requires buildah to be installed and may need root privileges + +// Check if buildah is installed +let buildah_exists = which("buildah"); +println(`Buildah exists: ${buildah_exists}`); + diff --git a/examples/buildah.rs b/examples/buildah.rs index 211e716..2648f6b 100644 --- a/examples/buildah.rs +++ b/examples/buildah.rs @@ -4,29 +4,36 @@ //! common container operations like creating containers, running commands, //! and managing images. -use sal::virt::buildah::{self, BuildahError}; +use sal::virt::buildah::{BuildahError, Builder}; use std::collections::HashMap; /// Run a complete buildah workflow example pub fn run_buildah_example() -> Result<(), BuildahError> { println!("Starting buildah example workflow..."); - // Step 1: Create a container from an image + // Step 1: Create a container from an image using the Builder println!("\n=== Creating container from fedora:latest ==="); - let result = buildah::from("fedora:latest")?; - let container_id = result.stdout.trim(); - println!("Created container: {}", container_id); + let mut builder = Builder::new("my-fedora-container", "fedora:latest")?; + + // Reset the builder to remove any existing container + println!("\n=== Resetting the builder to start fresh ==="); + builder.reset()?; + + // Create a new container (or continue with existing one) + println!("\n=== Creating container from fedora:latest ==="); + builder = Builder::new("my-fedora-container", "fedora:latest")?; + println!("Created container: {}", builder.container_id().unwrap_or(&"unknown".to_string())); // Step 2: Run a command in the container println!("\n=== Installing nginx in container ==="); // Use chroot isolation to avoid BPF issues - let install_result = buildah::bah_run_with_isolation(container_id, "dnf install -y nginx", "chroot")?; + let install_result = builder.run_with_isolation("dnf install -y nginx", "chroot")?; println!("{:#?}", install_result); println!("Installation output: {}", install_result.stdout); // Step 3: Copy a file into the container println!("\n=== Copying configuration file to container ==="); - buildah::bah_copy(container_id, "./example.conf", "/etc/example.conf").unwrap(); + builder.copy("./example.conf", "/etc/example.conf")?; // Step 4: Configure container metadata println!("\n=== Configuring container metadata ==="); @@ -34,19 +41,18 @@ pub fn run_buildah_example() -> Result<(), BuildahError> { config_options.insert("port".to_string(), "80".to_string()); config_options.insert("label".to_string(), "maintainer=example@example.com".to_string()); config_options.insert("entrypoint".to_string(), "/usr/sbin/nginx".to_string()); - buildah::bah_config(container_id, config_options)?; - buildah::config(container_id, config_options)?; + builder.config(config_options)?; println!("Container configured"); // Step 5: Commit the container to create a new image println!("\n=== Committing container to create image ==="); let image_name = "my-nginx:latest"; - buildah::image_commit(container_id, image_name, Some("docker"), true, true)?; + builder.commit(image_name)?; println!("Created image: {}", image_name); // Step 6: List images to verify our new image exists println!("\n=== Listing images ==="); - let images = buildah::images()?; + let images = Builder::images()?; println!("Found {} images:", images.len()); for image in images { println!(" ID: {}", image.id); @@ -56,9 +62,10 @@ pub fn run_buildah_example() -> Result<(), BuildahError> { println!(); } - // // Step 7: Clean up (optional in a real workflow) + // Step 7: Clean up (optional in a real workflow) println!("\n=== Cleaning up ==="); - buildah::image_remove(image_name).unwrap(); + Builder::image_remove(image_name)?; + builder.remove()?; println!("\nBuildah example workflow completed successfully!"); Ok(()) @@ -69,7 +76,7 @@ pub fn build_image_example() -> Result<(), BuildahError> { println!("Building an image from a Containerfile..."); // Use the build function with tag, context directory, and isolation to avoid BPF issues - let result = buildah::bah_build(Some("my-app:latest"), ".", "example_Dockerfile", Some("chroot"))?; + let result = Builder::build(Some("my-app:latest"), ".", "example_Dockerfile", Some("chroot"))?; println!("Build output: {}", result.stdout); println!("Image built successfully!"); @@ -83,18 +90,18 @@ pub fn registry_operations_example() -> Result<(), BuildahError> { // Pull an image println!("\n=== Pulling an image ==="); - buildah::image_pull("docker.io/library/alpine:latest", true)?; + Builder::image_pull("docker.io/library/alpine:latest", true)?; println!("Image pulled successfully"); // Tag the image println!("\n=== Tagging the image ==="); - buildah::image_tag("alpine:latest", "my-alpine:v1.0")?; + Builder::image_tag("alpine:latest", "my-alpine:v1.0")?; println!("Image tagged successfully"); // Push an image (this would typically go to a real registry) // println!("\n=== Pushing an image (example only) ==="); // println!("In a real scenario, you would push to a registry with:"); - // println!("buildah::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)"); + // println!("Builder::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)"); Ok(()) } diff --git a/examples/rhai_git_example.rs b/examples/rhai_git_example.rs new file mode 100644 index 0000000..d3c666c --- /dev/null +++ b/examples/rhai_git_example.rs @@ -0,0 +1,66 @@ +//! Example of using the Git module with Rhai +//! +//! This example demonstrates how to use the Git module functions +//! through the Rhai scripting language. + +use sal::rhai::{self, Engine}; + +fn main() -> Result<(), Box> { + // Create a new Rhai engine + let mut engine = Engine::new(); + + // Register SAL functions with the engine + rhai::register(&mut engine)?; + + // Run a Rhai script that uses Git functions + let script = r#" + // Print a header + print("=== Testing Git Module Functions ===\n"); + + // Test git_list function + print("Listing git repositories..."); + let repos = git_list(); + print(`Found ${repos.len()} repositories`); + + // Print the first few repositories + if repos.len() > 0 { + print("First few repositories:"); + let count = if repos.len() > 3 { 3 } else { repos.len() }; + for i in range(0, count) { + print(` - ${repos[i]}`); + } + } + + // Test find_matching_repos function + if repos.len() > 0 { + print("\nTesting repository search..."); + // Extract a part of the first repo name to search for + let repo_path = repos[0]; + let parts = repo_path.split("/"); + let repo_name = parts[parts.len() - 1]; + + print(`Searching for repositories containing "${repo_name}"`); + let matching = find_matching_repos(repo_name); + + print(`Found ${matching.len()} matching repositories`); + for repo in matching { + print(` - ${repo}`); + } + + // Check if a repository has changes + print("\nChecking for changes in repository..."); + let has_changes = git_has_changes(repo_path); + print(`Repository ${repo_path} has changes: ${has_changes}`); + } + + print("\n=== Git Module Test Complete ==="); + "#; + + // Evaluate the script + match engine.eval::<()>(script) { + Ok(_) => println!("Script executed successfully"), + Err(e) => eprintln!("Script execution error: {}", e), + } + + Ok(()) +} \ No newline at end of file diff --git a/rhaiexamples/04_buildah_operations.rhai b/rhaiexamples/04_buildah_operations.rhai index 503f394..19fba2a 100644 --- a/rhaiexamples/04_buildah_operations.rhai +++ b/rhaiexamples/04_buildah_operations.rhai @@ -6,12 +6,30 @@ let buildah_exists = which("buildah"); println(`Buildah exists: ${buildah_exists}`); +// Create a builder object +println("\nCreating a builder object:"); +let container_name = "my-container-example"; + +// Create a new builder +let builder = bah_new(container_name, "alpine:latest"); + +// Reset the builder to remove any existing container +println("\nResetting the builder to start fresh:"); +let reset_result = builder.reset(); +println(`Reset result: ${reset_result}`); + +// Create a new container after reset +println("\nCreating a new container after reset:"); +builder = bah_new(container_name, "alpine:latest"); +println(`Container created with ID: ${builder.container_id}`); +println(`Builder created with name: ${builder.name}, image: ${builder.image}`); + // List available images (only if buildah is installed) -println("Listing available container images:"); +println("\nListing available container images:"); // if ! buildah_exists != "" { // //EXIT // } -let images = bah_images(); +let images = builder.images(); println(`Found ${images.len()} images`); // Print image details (limited to 3) @@ -24,73 +42,65 @@ for img in images { count += 1; } -//Create a container from an image -println("\nCreating a container from alpine image:"); -let container = bah_from("alpine:latest"); -println(`Container result: success=${container.success}, code=${container.code}`); -println(`Container stdout: "${container.stdout}"`); -println(`Container stderr: "${container.stderr}"`); -let container_id = container.stdout; -println(`Container ID: ${container_id}`); - //Run a command in the container println("\nRunning a command in the container:"); -let run_result = bah_run(container_id, "echo 'Hello from container'"); +let run_result = builder.run("echo 'Hello from container'"); println(`Command output: ${run_result.stdout}`); //Add a file to the container println("\nAdding a file to the container:"); let test_file = "test_file.txt"; -run(`echo "Test content" > ${test_file}`); -let add_result = bah_add(container_id, test_file, "/"); +// Create the test file using Rhai's file_write function +file_write(test_file, "Test content"); +println(`Created test file: ${test_file}`); +println(`Created test file: ${test_file}`); +let add_result = builder.add(test_file, "/"); println(`Add result: ${add_result.success}`); //Commit the container to create a new image println("\nCommitting the container to create a new image:"); -let commit_result = bah_commit(container_id, "my-custom-image:latest"); +let commit_result = builder.commit("my-custom-image:latest"); println(`Commit result: ${commit_result.success}`); //Remove the container println("\nRemoving the container:"); -let remove_result = bah_remove(container_id); +let remove_result = builder.remove(); println(`Remove result: ${remove_result.success}`); //Clean up the test file delete(test_file); -// Demonstrate build options -println("\nDemonstrating build options:"); -let build_options = bah_new_build_options(); -build_options.tag = "example-image:latest"; -build_options.context_dir = "."; -build_options.file = "example_Dockerfile"; +// Demonstrate static methods +println("\nDemonstrating static methods:"); +println("Building an image from a Dockerfile:"); +let build_result = builder.build("example-image:latest", ".", "example_Dockerfile", "chroot"); +println(`Build result: ${build_result.success}`); -println("Build options configured:"); -println(` - Tag: ${build_options.tag}`); -println(` - Context: ${build_options.context_dir}`); -println(` - Dockerfile: ${build_options.file}`); +// Pull an image +println("\nPulling an image:"); +let pull_result = builder.image_pull("alpine:latest", true); +println(`Pull result: ${pull_result.success}`); -// Demonstrate commit options -println("\nDemonstrating commit options:"); -let commit_options = bah_new_commit_options(); -commit_options.format = "docker"; -commit_options.squash = true; -commit_options.rm = true; +// Skip commit options demonstration since we removed the legacy functions +println("\nSkipping commit options demonstration (legacy functions removed)"); -println("Commit options configured:"); -println(` - Format: ${commit_options.format}`); -println(` - Squash: ${commit_options.squash}`); -println(` - Remove container: ${commit_options.rm}`); +// Demonstrate config method +println("\nDemonstrating config method:"); +// Create a new container for config demonstration +println("Creating a new container for config demonstration:"); +builder = bah_new("config-demo-container", "alpine:latest"); +println(`Container created with ID: ${builder.container_id}`); -// Demonstrate config options -println("\nDemonstrating config options:"); -let config_options = bah_new_config_options(); -config_options.author = "Rhai Example"; -config_options.cmd = "/bin/sh -c 'echo Hello from Buildah'"; +let config_options = #{ + "author": "Rhai Example", + "cmd": "/bin/sh -c 'echo Hello from Buildah'" +}; +let config_result = builder.config(config_options); +println(`Config result: ${config_result.success}`); -println("Config options configured:"); -println(` - Author: ${config_options.author}`); -println(` - Command: ${config_options.cmd}`); +// Clean up the container +println("Removing the config demo container:"); +builder.remove(); "Buildah operations script completed successfully!" \ No newline at end of file diff --git a/src/docs/rhai/buildah.md b/src/docs/rhai/buildah.md index 666b278..13e7f00 100644 --- a/src/docs/rhai/buildah.md +++ b/src/docs/rhai/buildah.md @@ -2,6 +2,69 @@ The Buildah module provides functions for working with containers and images using the Buildah tool. Buildah helps you create and manage container images. +## Builder Pattern + +The Buildah module now supports a Builder pattern, which provides a more intuitive and flexible way to work with containers and images. + +### Creating a Builder + +```rhai +// Create a builder with a name and base image +let builder = bah_new("my-container", "alpine:latest"); + +// Access builder properties +let container_id = builder.container_id; +let name = builder.name; +let image = builder.image; +``` + +### Builder Methods + +The Builder object provides the following methods: + +- `run(command)`: Run a command in the container +- `run_with_isolation(command, isolation)`: Run a command with specified isolation +- `copy(source, dest)`: Copy files into the container +- `add(source, dest)`: Add files into the container +- `commit(image_name)`: Commit the container to an image +- `remove()`: Remove the container +- `reset()`: Remove the container and clear the container_id +- `config(options)`: Configure container metadata +- `images()`: List images in local storage +- `image_remove(image)`: Remove an image +- `image_pull(image, tls_verify)`: Pull an image from a registry +- `image_push(image, destination, tls_verify)`: Push an image to a registry +- `image_tag(image, new_name)`: Add a tag to an image +- `build(tag, context_dir, file, isolation)`: Build an image from a Dockerfile + +### Example + +```rhai +// Create a builder +let builder = bah_new("my-container", "alpine:latest"); + +// Reset the builder to remove any existing container +builder.reset(); + +// Create a new container +builder = bah_new("my-container", "alpine:latest"); + +// Run a command +let result = builder.run("echo 'Hello from container'"); +println(`Command output: ${result.stdout}`); + +// Add a file +file_write("test_file.txt", "Test content"); +builder.add("test_file.txt", "/"); + +// Commit to an image +builder.commit("my-custom-image:latest"); + +// Clean up +builder.remove(); +delete("test_file.txt"); +``` + ## Image Information ### Image Properties @@ -14,332 +77,42 @@ When working with images, you can access the following information: - `size`: The size of the image - `created`: When the image was created -## Container Functions +## Builder Methods -### `bah_from(image)` +### `bah_new(name, image)` -Creates a container from an image. +Creates a new Builder object for working with a container. **Parameters:** +- `name` (string): The name to give the container - `image` (string): The name or ID of the image to create the container from -**Returns:** The ID of the newly created container if successful. - -**Example:** - -```rhai -// Create a container from an image -let result = bah_from("alpine:latest"); -let container_id = result.stdout; -print(`Created container: ${container_id}`); -``` - -### `bah_run(container, command)` - -Runs a command in a container. - -**Parameters:** -- `container` (string): The container ID or name -- `command` (string): The command to run - -**Returns:** The output of the command if successful. +**Returns:** A Builder object if successful. **Example:** ```rhai -// Run a command in a container -let result = bah_run("my-container", "echo 'Hello from container'"); -print(result.stdout); +// Create a new Builder +let builder = bah_new("my-container", "alpine:latest"); ``` -### `bah_run_with_isolation(container, command, isolation)` +**Notes:** +- If a container with the given name already exists, it will be reused instead of creating a new one +- The Builder object provides methods for working with the container -Runs a command in a container with specified isolation. +### `reset()` -**Parameters:** -- `container` (string): The container ID or name -- `command` (string): The command to run -- `isolation` (string): The isolation type (e.g., "chroot", "rootless", "oci") +Resets a Builder by removing the container and clearing the container_id. This allows you to start fresh with the same Builder object. -**Returns:** The output of the command if successful. +**Returns:** Nothing. **Example:** ```rhai -// Run a command with specific isolation -let result = bah_run_with_isolation("my-container", "ls -la", "chroot"); -print(result.stdout); -``` - -### `bah_copy(container, source, dest)` - -Copies files into a container. - -**Parameters:** -- `container` (string): The container ID or name -- `source` (string): The source path on the host -- `dest` (string): The destination path in the container - -**Returns:** A success message if the copy operation worked. - -**Example:** -```rhai -// Copy a file into a container -bah_copy("my-container", "./app.js", "/app/app.js"); -``` - -### `bah_add(container, source, dest)` - -Adds files into a container. Similar to `bah_copy` but can also handle remote URLs. - -**Parameters:** -- `container` (string): The container ID or name -- `source` (string): The source path on the host or a URL -- `dest` (string): The destination path in the container - -**Returns:** A success message if the add operation worked. - -**Example:** -```rhai -// Add a file from a URL into a container -bah_add("my-container", "https://example.com/file.tar.gz", "/app/"); -``` - -### `bah_commit(container, image_name)` - -Commits a container to an image. - -**Parameters:** -- `container` (string): The container ID or name -- `image_name` (string): The name to give the new image - -**Returns:** A success message if the commit operation worked. - -**Example:** -```rhai -// Commit a container to an image -bah_commit("my-container", "my-image:latest"); -``` - -### `bah_remove(container)` - -Removes a container. - -**Parameters:** -- `container` (string): The container ID or name - -**Returns:** A success message if the container was removed. - -**Example:** -```rhai -// Remove a container -bah_remove("my-container"); -``` - -### `bah_list()` - -Lists containers. - -**Returns:** A list of containers if successful. - -**Example:** -```rhai -// List containers -let result = bah_list(); -print(result.stdout); -``` - -### `bah_new_build_options()` - -Creates a new map with default build options. - -**Returns:** A map with the following default options: -- `tag` (unit/null): The tag for the image (default: null) -- `context_dir` (string): The build context directory (default: ".") -- `file` (string): The Dockerfile path (default: "Dockerfile") -- `isolation` (unit/null): The isolation type (default: null) - -**Example:** -```rhai -// Create build options -let options = bah_new_build_options(); -``` - -### `bah_build(options)` - -Builds an image with options specified in a map. - -**Parameters:** -- `options` (map): A map of options created with `bah_new_build_options()` - -**Returns:** A success message if the build operation worked. - -**Example:** -```rhai -// Create and customize build options -let options = bah_new_build_options(); -options.tag = "my-image:latest"; -options.context_dir = "./app"; -options.file = "Dockerfile.prod"; -options.isolation = "chroot"; - -// Build an image with options -let result = bah_build(options); -``` - -## Image Functions - -### `bah_images()` - -Lists images in local storage. - -**Returns:** A list of images if successful. - -**Example:** -```rhai -// List images -let images = bah_images(); - -// Display image information -for image in images { - print(`ID: ${image.id}, Name: ${image.name}, Size: ${image.size}, Created: ${image.created}`); -} -``` - - -### `bah_image_remove(image)` - -Removes one or more images. - -**Parameters:** -- `image` (string): The image ID or name - -**Returns:** A success message if the image was removed. - -**Example:** -```rhai -// Remove an image -bah_image_remove("my-image:latest"); -``` - -### `bah_image_push(image, destination, tls_verify)` - -Pushes an image to a registry. - -**Parameters:** -- `image` (string): The image ID or name -- `destination` (string): The destination registry/repository -- `tls_verify` (boolean): Whether to verify TLS certificates - -**Returns:** A success message if the image was pushed. - -**Example:** -```rhai -// Push an image to a registry -bah_image_push("my-image:latest", "registry.example.com/my-repo/my-image:latest", true); -``` - -### `bah_image_tag(image, new_name)` - -Adds an additional name to a local image. - -**Parameters:** -- `image` (string): The image ID or name -- `new_name` (string): The new name to add - -**Returns:** A success message if the image was tagged. - -**Example:** -```rhai -// Tag an image with a new name -bah_image_tag("my-image:latest", "my-image:v1.0"); -``` - -### `bah_image_pull(image, tls_verify)` - -Pulls an image from a registry. - -**Parameters:** -- `image` (string): The image to pull -- `tls_verify` (boolean): Whether to verify TLS certificates - -**Returns:** A success message if the image was pulled. - -**Example:** -```rhai -// Pull an image from a registry -bah_image_pull("alpine:latest", true); -``` - -### `bah_new_commit_options()` - -Creates a new map with default commit options. - -**Returns:** A map with the following default options: -- `format` (unit/null): The format of the image (default: null) -- `squash` (boolean): Whether to squash layers (default: false) -- `rm` (boolean): Whether to remove the container after commit (default: false) - -**Example:** -```rhai -// Create commit options -let options = bah_new_commit_options(); -``` - -### `bah_image_commit(container, image_name, options)` - -Commits a container to an image with options specified in a map. - -**Parameters:** -- `container` (string): The container ID or name -- `image_name` (string): The name to give the new image -- `options` (map): A map of options created with `bah_new_commit_options()` - -**Returns:** A success message if the image was created. - -**Example:** -```rhai -// Create and customize commit options -let options = bah_new_commit_options(); -options.format = "docker"; -options.squash = true; -options.rm = true; - -// Commit a container to an image with options -let result = bah_image_commit("my-container", "my-image:latest", options); -``` - -### `bah_new_config_options()` - -Creates a new map for config options. - -**Returns:** An empty map to be filled with configuration options. - -**Example:** -```rhai -// Create config options -let options = bah_new_config_options(); -``` - -### `bah_config(container, options)` - -Configures a container with options specified in a map. - -**Parameters:** -- `container` (string): The container ID or name -- `options` (map): A map of options created with `bah_new_config_options()` - -**Returns:** A success message if the container was configured. - -**Example:** -```rhai -// Create and customize config options -let options = bah_new_config_options(); -options.author = "John Doe"; -options.cmd = "echo Hello"; -options.entrypoint = "/bin/sh -c"; -options.workingdir = "/app"; -options.env = "NODE_ENV=production"; -options.label = "version=1.0"; - -// Configure a container with options -let result = bah_config("my-container", options); +// Create a Builder +let builder = bah_new("my-container", "alpine:latest"); + +// Reset the Builder to remove the container +builder.reset(); + +// Create a new container with the same name +builder = bah_new("my-container", "alpine:latest"); ``` diff --git a/src/docs/rhai/git.md b/src/docs/rhai/git.md index 0d00990..9421334 100644 --- a/src/docs/rhai/git.md +++ b/src/docs/rhai/git.md @@ -2,112 +2,202 @@ This module provides Rhai wrappers for the Git functionality in SAL. -## Basic Git Operations +> **Note:** The constructor for GitTree has been renamed from `new()` to `gittree_new()` to avoid confusion with other constructors. This makes the interface more explicit and less likely to cause naming conflicts. -### Clone a Repository +## Object-Oriented Design + +The Git module follows an object-oriented design with two main classes: + +1. **GitTree** - Represents a collection of git repositories under a base path + - Created with `gittree_new(base_path)` + - Methods for listing, finding, and getting repositories + +2. **GitRepo** - Represents a single git repository + - Obtained from GitTree's `get()` method + - Methods for common git operations: pull, reset, push, commit + +This design allows for a more intuitive and flexible interface, with method chaining for complex operations. + +## Creating a GitTree + +The GitTree object is the main entry point for git operations. It represents a collection of git repositories under a base path. ```rhai -// Clone a repository to a standardized location in the user's home directory -let repo_path = git_clone("https://github.com/username/repo.git"); -print(`Repository cloned to: ${repo_path}`); +// Create a new GitTree with a base path +let git_tree = gittree_new("/root/code"); +print(`Created GitTree with base path: /home/user/code`); ``` -### List Repositories +## Finding Repositories + +### List All Repositories ```rhai -// List all git repositories in the user's ~/code directory -let repos = git_list(); -print("Found repositories:"); +// List all git repositories under the base path +let repos = git_tree.list(); +print(`Found ${repos.len()} repositories`); + +// Print the repositories for repo in repos { print(` - ${repo}`); } ``` -### Find Repositories +### Find Repositories Matching a Pattern ```rhai // Find repositories matching a pattern // Use a wildcard (*) suffix to find multiple matches -let matching_repos = find_matching_repos("my-project*"); +let matching_repos = git_tree.find("my-project*"); print("Matching repositories:"); for repo in matching_repos { print(` - ${repo}`); } // Find a specific repository (must match exactly one) -let specific_repo = find_matching_repos("unique-project")[0]; +let specific_repo = git_tree.find("unique-project")[0]; print(`Found specific repository: ${specific_repo}`); ``` +## Working with Repositories + +### Get Repository Objects + +```rhai +// Get GitRepo objects for repositories matching a pattern +let repos = git_tree.get("my-project*"); +print(`Found ${repos.len()} repositories`); + +// Get a specific repository +let repo = git_tree.get("unique-project")[0]; +print(`Working with repository: ${repo.path()}`); +``` + +### Clone a Repository + +```rhai +// Clone a repository by URL +// This will clone the repository to the base path of the GitTree +let repos = git_tree.get("https://github.com/username/repo.git"); +let repo = repos[0]; +print(`Repository cloned to: ${repo.path()}`); +``` + ### Check for Changes ```rhai // Check if a repository has uncommitted changes -let repo_path = "/path/to/repo"; -if git_has_changes(repo_path) { +let repo = git_tree.get("my-project")[0]; +if repo.has_changes() { print("Repository has uncommitted changes"); } else { print("Repository is clean"); } ``` -## Repository Updates +## Repository Operations -### Update a Repository +### Pull Changes ```rhai -// Update a repository by pulling the latest changes +// Pull the latest changes from the remote // This will fail if there are uncommitted changes -let result = git_update("my-project"); -print(result); +let repo = git_tree.get("my-project")[0]; +let result = repo.pull(); +print("Repository updated successfully"); ``` -### Force Update a Repository +### Reset Local Changes ```rhai -// Force update a repository by discarding local changes and pulling the latest changes -let result = git_update_force("my-project"); -print(result); +// Reset any local changes in the repository +let repo = git_tree.get("my-project")[0]; +let result = repo.reset(); +print("Repository reset successfully"); ``` -### Commit and Update +### Commit Changes ```rhai -// Commit changes in a repository and then update it by pulling the latest changes -let result = git_update_commit("my-project", "Fix bug in login form"); -print(result); +// Commit changes in the repository +let repo = git_tree.get("my-project")[0]; +let result = repo.commit("Fix bug in login form"); +print("Changes committed successfully"); ``` -### Commit and Push +### Push Changes ```rhai -// Commit changes in a repository and push them to the remote -let result = git_update_commit_push("my-project", "Add new feature"); -print(result); +// Push changes to the remote +let repo = git_tree.get("my-project")[0]; +let result = repo.push(); +print("Changes pushed successfully"); ``` +## Method Chaining + +The GitRepo methods can be chained together for more complex operations: + +```rhai +// Commit changes and push them to the remote +let repo = git_tree.get("my-project")[0]; +let result = repo.commit("Add new feature").push(); +print("Changes committed and pushed successfully"); + +// Reset local changes, pull the latest changes, and commit new changes +let repo = git_tree.get("my-project")[0]; +let result = repo.reset().pull().commit("Update dependencies"); +print("Repository updated successfully"); +``` ## Complete Example ```rhai +// Create a new GitTree +let home_dir = env("HOME"); +let git_tree = gittree_new(`${home_dir}/code`); + // Clone a repository -let repo_url = "https://github.com/username/example-repo.git"; -let repo_path = git_clone(repo_url); -print(`Cloned repository to: ${repo_path}`); +let repos = git_tree.get("https://github.com/username/example-repo.git"); +let repo = repos[0]; +print(`Cloned repository to: ${repo.path()}`); // Make some changes (using OS module functions) -let file_path = `${repo_path}/README.md`; +let file_path = `${repo.path()}/README.md`; let content = "# Example Repository\n\nThis is an example repository."; write_file(file_path, content); // Commit and push the changes -let commit_message = "Update README.md"; -let result = git_update_commit_push(repo_path, commit_message); -print(result); +let result = repo.commit("Update README.md").push(); +print("Changes committed and pushed successfully"); // List all repositories -let repos = git_list(); +let all_repos = git_tree.list(); print("All repositories:"); -for repo in repos { - print(` - ${repo}`); -} \ No newline at end of file +for repo_path in all_repos { + print(` - ${repo_path}`); +} + +## Error Handling + +All methods in the Git module return a Result type, which means they can either succeed or fail with an error. If an error occurs, it will be propagated to the Rhai script as a runtime error. + +For example, if you try to clone a repository that doesn't exist: + +```rhai +// Try to clone a non-existent repository +try { + let git_tree = gittree_new("/root/code"); + let repos = git_tree.get("https://github.com/nonexistent/repo.git"); + print("This will not be executed if the repository doesn't exist"); +} catch(err) { + print(`Error: ${err}`); // Will print the error message from git +} +``` + +Common errors include: +- Invalid URL +- Repository not found +- Authentication failure +- Network issues +- Local changes exist when trying to pull diff --git a/src/examples/git_test.rs b/src/examples/git_test.rs index d3c666c..f6b7141 100644 --- a/src/examples/git_test.rs +++ b/src/examples/git_test.rs @@ -17,9 +17,14 @@ fn main() -> Result<(), Box> { // Print a header print("=== Testing Git Module Functions ===\n"); - // Test git_list function - print("Listing git repositories..."); - let repos = git_list(); + // Create a new GitTree object + let home_dir = env("HOME"); + let git_tree = gittree_new(`${home_dir}/code`); + print(`Created GitTree with base path: ${home_dir}/code`); + + // Test list method + print("\nListing git repositories..."); + let repos = git_tree.list(); print(`Found ${repos.len()} repositories`); // Print the first few repositories @@ -31,7 +36,7 @@ fn main() -> Result<(), Box> { } } - // Test find_matching_repos function + // Test find method if repos.len() > 0 { print("\nTesting repository search..."); // Extract a part of the first repo name to search for @@ -40,17 +45,35 @@ fn main() -> Result<(), Box> { let repo_name = parts[parts.len() - 1]; print(`Searching for repositories containing "${repo_name}"`); - let matching = find_matching_repos(repo_name); + let matching = git_tree.find(repo_name + "*"); print(`Found ${matching.len()} matching repositories`); for repo in matching { print(` - ${repo}`); } - // Check if a repository has changes - print("\nChecking for changes in repository..."); - let has_changes = git_has_changes(repo_path); - print(`Repository ${repo_path} has changes: ${has_changes}`); + // Test get method + print("\nTesting get method..."); + let git_repos = git_tree.get(repo_name); + print(`Found ${git_repos.len()} GitRepo objects`); + + // Test GitRepo methods + if git_repos.len() > 0 { + let git_repo = git_repos[0]; + print(`\nTesting GitRepo methods on: ${git_repo.path()}`); + + // Check if a repository has changes + print("Checking for changes in repository..."); + let has_changes = git_repo.has_changes(); + print(`Repository has changes: ${has_changes}`); + + // Test method chaining (only if there are no changes to avoid errors) + if !has_changes { + print("\nTesting method chaining (pull)..."); + let result = git_repo.pull(); + print("Pull operation completed successfully"); + } + } } print("\n=== Git Module Test Complete ==="); diff --git a/src/examples/rhai_git_example.rs b/src/examples/rhai_git_example.rs index d3c666c..f6b7141 100644 --- a/src/examples/rhai_git_example.rs +++ b/src/examples/rhai_git_example.rs @@ -17,9 +17,14 @@ fn main() -> Result<(), Box> { // Print a header print("=== Testing Git Module Functions ===\n"); - // Test git_list function - print("Listing git repositories..."); - let repos = git_list(); + // Create a new GitTree object + let home_dir = env("HOME"); + let git_tree = gittree_new(`${home_dir}/code`); + print(`Created GitTree with base path: ${home_dir}/code`); + + // Test list method + print("\nListing git repositories..."); + let repos = git_tree.list(); print(`Found ${repos.len()} repositories`); // Print the first few repositories @@ -31,7 +36,7 @@ fn main() -> Result<(), Box> { } } - // Test find_matching_repos function + // Test find method if repos.len() > 0 { print("\nTesting repository search..."); // Extract a part of the first repo name to search for @@ -40,17 +45,35 @@ fn main() -> Result<(), Box> { let repo_name = parts[parts.len() - 1]; print(`Searching for repositories containing "${repo_name}"`); - let matching = find_matching_repos(repo_name); + let matching = git_tree.find(repo_name + "*"); print(`Found ${matching.len()} matching repositories`); for repo in matching { print(` - ${repo}`); } - // Check if a repository has changes - print("\nChecking for changes in repository..."); - let has_changes = git_has_changes(repo_path); - print(`Repository ${repo_path} has changes: ${has_changes}`); + // Test get method + print("\nTesting get method..."); + let git_repos = git_tree.get(repo_name); + print(`Found ${git_repos.len()} GitRepo objects`); + + // Test GitRepo methods + if git_repos.len() > 0 { + let git_repo = git_repos[0]; + print(`\nTesting GitRepo methods on: ${git_repo.path()}`); + + // Check if a repository has changes + print("Checking for changes in repository..."); + let has_changes = git_repo.has_changes(); + print(`Repository has changes: ${has_changes}`); + + // Test method chaining (only if there are no changes to avoid errors) + if !has_changes { + print("\nTesting method chaining (pull)..."); + let result = git_repo.pull(); + print("Pull operation completed successfully"); + } + } } print("\n=== Git Module Test Complete ==="); diff --git a/src/examples/run_test_git.rs b/src/examples/run_test_git.rs new file mode 100644 index 0000000..e410804 --- /dev/null +++ b/src/examples/run_test_git.rs @@ -0,0 +1,27 @@ +//! Example of running the test_git.rhai script +//! +//! This example demonstrates how to run the test_git.rhai script +//! through the Rhai scripting engine. + +use sal::rhai::{self, Engine}; +use std::fs; +use std::error::Error; + +fn main() -> Result<(), Box> { + // Create a new Rhai engine + let mut engine = Engine::new(); + + // Register SAL functions with the engine + rhai::register(&mut engine)?; + + // Read the test script + let script = fs::read_to_string("src/test_git.rhai")?; + + // Evaluate the script + match engine.eval::<()>(&script) { + Ok(_) => println!("Script executed successfully"), + Err(e) => eprintln!("Script execution error: {}", e), + } + + Ok(()) +} \ No newline at end of file diff --git a/src/examples/simple_git_test.rs b/src/examples/simple_git_test.rs new file mode 100644 index 0000000..a174c01 --- /dev/null +++ b/src/examples/simple_git_test.rs @@ -0,0 +1,27 @@ +//! Simple example of using the Git module with Rhai +//! +//! This example demonstrates how to use the Git module functions +//! through the Rhai scripting language. + +use sal::rhai::{self, Engine}; +use std::fs; +use std::error::Error; + +fn main() -> Result<(), Box> { + // Create a new Rhai engine + let mut engine = Engine::new(); + + // Register SAL functions with the engine + rhai::register(&mut engine)?; + + // Read the test script + let script = fs::read_to_string("simple_git_test.rhai")?; + + // Evaluate the script + match engine.eval::<()>(&script) { + Ok(_) => println!("Script executed successfully"), + Err(e) => eprintln!("Script execution error: {}", e), + } + + Ok(()) +} \ No newline at end of file diff --git a/src/git/git.rs b/src/git/git.rs index 7f0afca..c721b59 100644 --- a/src/git/git.rs +++ b/src/git/git.rs @@ -11,6 +11,7 @@ use std::error::Error; pub enum GitError { GitNotInstalled(std::io::Error), InvalidUrl(String), + InvalidBasePath(String), HomeDirectoryNotFound(std::env::VarError), FileSystemError(std::io::Error), GitCommandFailed(String), @@ -28,6 +29,7 @@ impl fmt::Display for GitError { match self { GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e), GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url), + GitError::InvalidBasePath(path) => write!(f, "Invalid base path: {}", path), GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e), GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e), GitError::GitCommandFailed(e) => write!(f, "{}", e), @@ -55,98 +57,21 @@ impl Error for GitError { } } -// Git utility functions - -/** - * Clones a git repository to a standardized location in the user's home directory. - * - * # Arguments - * - * * `url` - The URL of the git repository to clone. Can be in HTTPS format - * (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git). - * - * # Returns - * - * * `Ok(String)` - The path where the repository was cloned, formatted as - * ~/code/server/account/repo (e.g., ~/code/github.com/username/repo). - * * `Err(GitError)` - An error if the clone operation failed. - * - * # Examples - * - * ``` - * let repo_path = git_clone("https://github.com/username/repo.git")?; - * println!("Repository cloned to: {}", repo_path); - * ``` - */ -pub fn git_clone(url: &str) -> Result { - // Check if git is installed - let _git_check = Command::new("git") - .arg("--version") - .output() - .map_err(GitError::GitNotInstalled)?; - - // Parse the URL to determine the clone path - let (server, account, repo) = parse_git_url(url); - if server.is_empty() || account.is_empty() || repo.is_empty() { - return Err(GitError::InvalidUrl(url.to_string())); - } - - // Create the target directory - let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?; - - let clone_path = format!("{}/code/{}/{}/{}", home_dir, server, account, repo); - let clone_dir = Path::new(&clone_path); - - // Check if repo already exists - if clone_dir.exists() { - return Ok(format!("Repository already exists at {}", clone_path)); - } - - // Create parent directory - if let Some(parent) = clone_dir.parent() { - fs::create_dir_all(parent).map_err(GitError::FileSystemError)?; - } - - // Clone the repository - let output = Command::new("git") - .args(&["clone", "--depth", "1", url, &clone_path]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if output.status.success() { - Ok(clone_path) - } else { - let error = String::from_utf8_lossy(&output.stderr); - Err(GitError::GitCommandFailed(format!("Git clone error: {}", error))) - } -} - -/** - * Parses a git URL to extract the server, account, and repository name. - * - * # Arguments - * - * * `url` - The URL of the git repository to parse. Can be in HTTPS format - * (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git). - * - * # Returns - * - * A tuple containing: - * * `server` - The server name (e.g., "github.com") - * * `account` - The account or organization name (e.g., "username") - * * `repo` - The repository name (e.g., "repo") - * - * If the URL cannot be parsed, all three values will be empty strings. - * - * # Examples - * - * ``` - * let (server, account, repo) = parse_git_url("https://github.com/username/repo.git"); - * assert_eq!(server, "github.com"); - * assert_eq!(account, "username"); - * assert_eq!(repo, "repo"); - * ``` - */ +/// Parses a git URL to extract the server, account, and repository name. +/// +/// # Arguments +/// +/// * `url` - The URL of the git repository to parse. Can be in HTTPS format +/// (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git). +/// +/// # Returns +/// +/// A tuple containing: +/// * `server` - The server name (e.g., "github.com") +/// * `account` - The account or organization name (e.g., "username") +/// * `repo` - The repository name (e.g., "repo") +/// +/// If the URL cannot be parsed, all three values will be empty strings. pub fn parse_git_url(url: &str) -> (String, String, String) { // HTTP(S) URL format: https://github.com/username/repo.git let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap(); @@ -171,427 +96,394 @@ pub fn parse_git_url(url: &str) -> (String, String, String) { (String::new(), String::new(), String::new()) } -/** - * Lists all git repositories found in the user's ~/code directory. - * - * This function searches for directories containing a .git subdirectory, - * which indicates a git repository. - * - * # Returns - * - * * `Ok(Vec)` - A vector of paths to git repositories - * * `Err(GitError)` - An error if the operation failed - * - * # Examples - * - * ``` - * let repos = git_list()?; - * for repo in repos { - * println!("Found repository: {}", repo); - * } - * ``` - */ -pub fn git_list() -> Result, GitError> { - // Get home directory - let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?; - - let code_dir = format!("{}/code", home_dir); - let code_path = Path::new(&code_dir); - - if !code_path.exists() || !code_path.is_dir() { - return Ok(Vec::new()); +/// Checks if git is installed on the system. +/// +/// # Returns +/// +/// * `Ok(())` - If git is installed +/// * `Err(GitError)` - If git is not installed +fn check_git_installed() -> Result<(), GitError> { + Command::new("git") + .arg("--version") + .output() + .map_err(GitError::GitNotInstalled)?; + Ok(()) +} + +/// Represents a collection of git repositories under a base path. +#[derive(Clone)] +pub struct GitTree { + base_path: String, +} + +impl GitTree { + /// Creates a new GitTree with the specified base path. + /// + /// # Arguments + /// + /// * `base_path` - The base path where all git repositories are located + /// + /// # Returns + /// + /// * `Ok(GitTree)` - A new GitTree instance + /// * `Err(GitError)` - If the base path is invalid or cannot be created + pub fn new(base_path: &str) -> Result { + // Check if git is installed + check_git_installed()?; + + // Validate the base path + let path = Path::new(base_path); + if !path.exists() { + fs::create_dir_all(path).map_err(|e| { + GitError::FileSystemError(e) + })?; + } else if !path.is_dir() { + return Err(GitError::InvalidBasePath(base_path.to_string())); + } + + Ok(GitTree { + base_path: base_path.to_string(), + }) } - let mut repos = Vec::new(); - - // Find all directories with .git subdirectories - let output = Command::new("find") - .args(&[&code_dir, "-type", "d", "-name", ".git"]) - .output() - .map_err(GitError::CommandExecutionError)?; + /// Lists all git repositories under the base path. + /// + /// # Returns + /// + /// * `Ok(Vec)` - A vector of paths to git repositories + /// * `Err(GitError)` - If the operation failed + pub fn list(&self) -> Result, GitError> { + let base_path = Path::new(&self.base_path); - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - for line in stdout.lines() { - // Get the parent directory of .git which is the repo root - if let Some(parent) = Path::new(line).parent() { - if let Some(path_str) = parent.to_str() { - repos.push(path_str.to_string()); + if !base_path.exists() || !base_path.is_dir() { + return Ok(Vec::new()); + } + + let mut repos = Vec::new(); + + // Find all directories with .git subdirectories + let output = Command::new("find") + .args(&[&self.base_path, "-type", "d", "-name", ".git"]) + .output() + .map_err(GitError::CommandExecutionError)?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + // Get the parent directory of .git which is the repo root + if let Some(parent) = Path::new(line).parent() { + if let Some(path_str) = parent.to_str() { + repos.push(path_str.to_string()); + } } } - } - } else { - let error = String::from_utf8_lossy(&output.stderr); - return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error))); - } - - Ok(repos) -} - -/** - * Checks if a git repository has uncommitted changes. - * - * # Arguments - * - * * `repo_path` - The path to the git repository - * - * # Returns - * - * * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise - * * `Err(GitError)` - An error if the operation failed - * - * # Examples - * - * ``` - * if has_git_changes("/path/to/repo")? { - * println!("Repository has uncommitted changes"); - * } else { - * println!("Repository is clean"); - * } - * ``` - */ -pub fn has_git_changes(repo_path: &str) -> Result { - let output = Command::new("git") - .args(&["-C", repo_path, "status", "--porcelain"]) - .output() - .map_err(GitError::CommandExecutionError)?; - - Ok(!output.stdout.is_empty()) -} - -/** - * Finds repositories matching a pattern or partial path. - * - * # Arguments - * - * * `pattern` - The pattern to match against repository paths - * - If the pattern ends with '*', all matching repositories are returned - * - Otherwise, exactly one matching repository must be found - * - * # Returns - * - * * `Ok(Vec)` - A vector of paths to matching repositories - * * `Err(GitError)` - An error if no matching repositories are found, - * or if multiple repositories match a non-wildcard pattern - * - * # Examples - * - * ``` - * // Find all repositories containing "project" - * let repos = find_matching_repos("project*")?; - * - * // Find exactly one repository containing "unique-project" - * let repo = find_matching_repos("unique-project")?[0]; - * ``` - */ -pub fn find_matching_repos(pattern: &str) -> Result, GitError> { - // Get all repos - let repos = git_list()?; - - if repos.is_empty() { - return Err(GitError::NoRepositoriesFound); - } - - // Check if pattern ends with wildcard - if pattern.ends_with('*') { - let search_pattern = &pattern[0..pattern.len()-1]; // Remove the * - let matching: Vec = repos.iter() - .filter(|repo| repo.contains(search_pattern)) - .cloned() - .collect(); - - if matching.is_empty() { - return Err(GitError::RepositoryNotFound(pattern.to_string())); - } - - Ok(matching) - } else { - // No wildcard, need to find exactly one match - let matching: Vec = repos.iter() - .filter(|repo| repo.contains(pattern)) - .cloned() - .collect(); - - match matching.len() { - 0 => Err(GitError::RepositoryNotFound(pattern.to_string())), - 1 => Ok(matching), - _ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())), - } - } -} - -/** - * Updates a git repository by pulling the latest changes. - * - * This function will fail if there are uncommitted changes in the repository. - * - * # Arguments - * - * * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository - * - * # Returns - * - * * `Ok(String)` - A success message indicating the repository was updated - * * `Err(GitError)` - An error if the update failed - * - * # Examples - * - * ``` - * let result = git_update("my-project")?; - * println!("{}", result); // "Successfully updated repository at /home/user/code/github.com/user/my-project" - * ``` - */ -pub fn git_update(repo_path: &str) -> Result { - // If repo_path may be a partial path, find the matching repository - let repos = find_matching_repos(repo_path)?; - - // Should only be one repository at this point - let actual_path = &repos[0]; - - // Check if repository exists and is a git repository - let git_dir = Path::new(actual_path).join(".git"); - if !git_dir.exists() || !git_dir.is_dir() { - return Err(GitError::NotAGitRepository(actual_path.clone())); - } - - // Check for local changes - if has_git_changes(actual_path)? { - return Err(GitError::LocalChangesExist(actual_path.clone())); - } - - // Pull the latest changes - let output = Command::new("git") - .args(&["-C", actual_path, "pull"]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - if stdout.contains("Already up to date") { - Ok(format!("Repository already up to date at {}", actual_path)) } else { - Ok(format!("Successfully updated repository at {}", actual_path)) + let error = String::from_utf8_lossy(&output.stderr); + return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error))); + } + + Ok(repos) + } + + /// Finds repositories matching a pattern or partial path. + /// + /// # Arguments + /// + /// * `pattern` - The pattern to match against repository paths + /// - If the pattern ends with '*', all matching repositories are returned + /// - Otherwise, exactly one matching repository must be found + /// + /// # Returns + /// + /// * `Ok(Vec)` - A vector of paths to matching repositories + /// * `Err(GitError)` - If no matching repositories are found, + /// or if multiple repositories match a non-wildcard pattern + pub fn find(&self, pattern: &str) -> Result, GitError> { + // Get all repos + let repos = self.list()?; + + if repos.is_empty() { + return Err(GitError::NoRepositoriesFound); + } + + // Check if pattern ends with wildcard + if pattern.ends_with('*') { + let search_pattern = &pattern[0..pattern.len()-1]; // Remove the * + let matching: Vec = repos.iter() + .filter(|repo| repo.contains(search_pattern)) + .cloned() + .collect(); + + if matching.is_empty() { + return Err(GitError::RepositoryNotFound(pattern.to_string())); + } + + Ok(matching) + } else { + // No wildcard, need to find exactly one match + let matching: Vec = repos.iter() + .filter(|repo| repo.contains(pattern)) + .cloned() + .collect(); + + match matching.len() { + 0 => Err(GitError::RepositoryNotFound(pattern.to_string())), + 1 => Ok(matching), + _ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())), + } + } + } + + /// Gets one or more GitRepo objects based on a path pattern or URL. + /// + /// # Arguments + /// + /// * `path_or_url` - The path pattern to match against repository paths or a git URL + /// - If it's a URL, the repository will be cloned if it doesn't exist + /// - If it's a path pattern, it will find matching repositories + /// + /// # Returns + /// + /// * `Ok(Vec)` - A vector of GitRepo objects + /// * `Err(GitError)` - If no matching repositories are found or the clone operation failed + pub fn get(&self, path_or_url: &str) -> Result, GitError> { + // Check if it's a URL + if path_or_url.starts_with("http") || path_or_url.starts_with("git@") { + // Parse the URL + let (server, account, repo) = parse_git_url(path_or_url); + if server.is_empty() || account.is_empty() || repo.is_empty() { + return Err(GitError::InvalidUrl(path_or_url.to_string())); + } + + // Create the target directory + let clone_path = format!("{}/{}/{}/{}", self.base_path, server, account, repo); + let clone_dir = Path::new(&clone_path); + + // Check if repo already exists + if clone_dir.exists() { + return Ok(vec![GitRepo::new(clone_path)]); + } + + // Create parent directory + if let Some(parent) = clone_dir.parent() { + fs::create_dir_all(parent).map_err(GitError::FileSystemError)?; + } + + // Clone the repository + let output = Command::new("git") + .args(&["clone", "--depth", "1", path_or_url, &clone_path]) + .output() + .map_err(GitError::CommandExecutionError)?; + + if output.status.success() { + Ok(vec![GitRepo::new(clone_path)]) + } else { + let error = String::from_utf8_lossy(&output.stderr); + Err(GitError::GitCommandFailed(format!("Git clone error: {}", error))) + } + } else { + // It's a path pattern, find matching repositories + let repo_paths = self.find(path_or_url)?; + + // Convert paths to GitRepo objects + let repos: Vec = repo_paths.into_iter() + .map(GitRepo::new) + .collect(); + + Ok(repos) } - } else { - let error = String::from_utf8_lossy(&output.stderr); - Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) } } -/** - * Force updates a git repository by discarding local changes and pulling the latest changes. - * - * This function will reset any uncommitted changes and clean untracked files before pulling. - * - * # Arguments - * - * * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository - * - * # Returns - * - * * `Ok(String)` - A success message indicating the repository was force-updated - * * `Err(GitError)` - An error if the update failed - * - * # Examples - * - * ``` - * let result = git_update_force("my-project")?; - * println!("{}", result); // "Successfully force-updated repository at /home/user/code/github.com/user/my-project" - * ``` - */ -pub fn git_update_force(repo_path: &str) -> Result { - // If repo_path may be a partial path, find the matching repository - let repos = find_matching_repos(repo_path)?; - - // Should only be one repository at this point - let actual_path = &repos[0]; - - // Check if repository exists and is a git repository - let git_dir = Path::new(actual_path).join(".git"); - if !git_dir.exists() || !git_dir.is_dir() { - return Err(GitError::NotAGitRepository(actual_path.clone())); +/// Represents a git repository. +pub struct GitRepo { + path: String, +} + +impl GitRepo { + /// Creates a new GitRepo with the specified path. + /// + /// # Arguments + /// + /// * `path` - The path to the git repository + pub fn new(path: String) -> Self { + GitRepo { path } } - // Reset any local changes - let reset_output = Command::new("git") - .args(&["-C", actual_path, "reset", "--hard", "HEAD"]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if !reset_output.status.success() { - let error = String::from_utf8_lossy(&reset_output.stderr); - return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error))); + /// Gets the path of the repository. + /// + /// # Returns + /// + /// * The path to the git repository + pub fn path(&self) -> &str { + &self.path } - - // Clean untracked files - let clean_output = Command::new("git") - .args(&["-C", actual_path, "clean", "-fd"]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if !clean_output.status.success() { - let error = String::from_utf8_lossy(&clean_output.stderr); - return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error))); + + /// Checks if the repository has uncommitted changes. + /// + /// # Returns + /// + /// * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise + /// * `Err(GitError)` - If the operation failed + pub fn has_changes(&self) -> Result { + let output = Command::new("git") + .args(&["-C", &self.path, "status", "--porcelain"]) + .output() + .map_err(GitError::CommandExecutionError)?; + + Ok(!output.stdout.is_empty()) } + + /// Pulls the latest changes from the remote repository. + /// + /// # Returns + /// + /// * `Ok(Self)` - The GitRepo object for method chaining + /// * `Err(GitError)` - If the pull operation failed + pub fn pull(&self) -> Result { + // Check if repository exists and is a git repository + let git_dir = Path::new(&self.path).join(".git"); + if !git_dir.exists() || !git_dir.is_dir() { + return Err(GitError::NotAGitRepository(self.path.clone())); + } + + // Check for local changes + if self.has_changes()? { + return Err(GitError::LocalChangesExist(self.path.clone())); + } - // Pull the latest changes - let pull_output = Command::new("git") - .args(&["-C", actual_path, "pull"]) - .output() - .map_err(GitError::CommandExecutionError)?; + // Pull the latest changes + let output = Command::new("git") + .args(&["-C", &self.path, "pull"]) + .output() + .map_err(GitError::CommandExecutionError)?; - if pull_output.status.success() { - Ok(format!("Successfully force-updated repository at {}", actual_path)) - } else { - let error = String::from_utf8_lossy(&pull_output.stderr); - Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) + if output.status.success() { + Ok(self.clone()) + } else { + let error = String::from_utf8_lossy(&output.stderr); + Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) + } + } + + /// Resets any local changes in the repository. + /// + /// # Returns + /// + /// * `Ok(Self)` - The GitRepo object for method chaining + /// * `Err(GitError)` - If the reset operation failed + pub fn reset(&self) -> Result { + // Check if repository exists and is a git repository + let git_dir = Path::new(&self.path).join(".git"); + if !git_dir.exists() || !git_dir.is_dir() { + return Err(GitError::NotAGitRepository(self.path.clone())); + } + + // Reset any local changes + let reset_output = Command::new("git") + .args(&["-C", &self.path, "reset", "--hard", "HEAD"]) + .output() + .map_err(GitError::CommandExecutionError)?; + + if !reset_output.status.success() { + let error = String::from_utf8_lossy(&reset_output.stderr); + return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error))); + } + + // Clean untracked files + let clean_output = Command::new("git") + .args(&["-C", &self.path, "clean", "-fd"]) + .output() + .map_err(GitError::CommandExecutionError)?; + + if !clean_output.status.success() { + let error = String::from_utf8_lossy(&clean_output.stderr); + return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error))); + } + + Ok(self.clone()) + } + + /// Commits changes in the repository. + /// + /// # Arguments + /// + /// * `message` - The commit message + /// + /// # Returns + /// + /// * `Ok(Self)` - The GitRepo object for method chaining + /// * `Err(GitError)` - If the commit operation failed + pub fn commit(&self, message: &str) -> Result { + // Check if repository exists and is a git repository + let git_dir = Path::new(&self.path).join(".git"); + if !git_dir.exists() || !git_dir.is_dir() { + return Err(GitError::NotAGitRepository(self.path.clone())); + } + + // Check for local changes + if !self.has_changes()? { + return Ok(self.clone()); + } + + // Add all changes + let add_output = Command::new("git") + .args(&["-C", &self.path, "add", "."]) + .output() + .map_err(GitError::CommandExecutionError)?; + + if !add_output.status.success() { + let error = String::from_utf8_lossy(&add_output.stderr); + return Err(GitError::GitCommandFailed(format!("Git add error: {}", error))); + } + + // Commit the changes + let commit_output = Command::new("git") + .args(&["-C", &self.path, "commit", "-m", message]) + .output() + .map_err(GitError::CommandExecutionError)?; + + if !commit_output.status.success() { + let error = String::from_utf8_lossy(&commit_output.stderr); + return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error))); + } + + Ok(self.clone()) + } + + /// Pushes changes to the remote repository. + /// + /// # Returns + /// + /// * `Ok(Self)` - The GitRepo object for method chaining + /// * `Err(GitError)` - If the push operation failed + pub fn push(&self) -> Result { + // Check if repository exists and is a git repository + let git_dir = Path::new(&self.path).join(".git"); + if !git_dir.exists() || !git_dir.is_dir() { + return Err(GitError::NotAGitRepository(self.path.clone())); + } + + // Push the changes + let push_output = Command::new("git") + .args(&["-C", &self.path, "push"]) + .output() + .map_err(GitError::CommandExecutionError)?; + + if push_output.status.success() { + Ok(self.clone()) + } else { + let error = String::from_utf8_lossy(&push_output.stderr); + Err(GitError::GitCommandFailed(format!("Git push error: {}", error))) + } } } -/** - * Commits changes in a git repository and then updates it by pulling the latest changes. - * - * # Arguments - * - * * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository - * * `message` - The commit message - * - * # Returns - * - * * `Ok(String)` - A success message indicating the repository was committed and updated - * * `Err(GitError)` - An error if the operation failed - * - * # Examples - * - * ``` - * let result = git_update_commit("my-project", "Fix bug in login form")?; - * println!("{}", result); // "Successfully committed and updated repository at /home/user/code/github.com/user/my-project" - * ``` - */ -pub fn git_update_commit(repo_path: &str, message: &str) -> Result { - // If repo_path may be a partial path, find the matching repository - let repos = find_matching_repos(repo_path)?; - - // Should only be one repository at this point - let actual_path = &repos[0]; - - // Check if repository exists and is a git repository - let git_dir = Path::new(actual_path).join(".git"); - if !git_dir.exists() || !git_dir.is_dir() { - return Err(GitError::NotAGitRepository(actual_path.clone())); - } - - // Check for local changes - if !has_git_changes(actual_path)? { - return Ok(format!("No changes to commit in repository at {}", actual_path)); - } - - // Add all changes - let add_output = Command::new("git") - .args(&["-C", actual_path, "add", "."]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if !add_output.status.success() { - let error = String::from_utf8_lossy(&add_output.stderr); - return Err(GitError::GitCommandFailed(format!("Git add error: {}", error))); - } - - // Commit the changes - let commit_output = Command::new("git") - .args(&["-C", actual_path, "commit", "-m", message]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if !commit_output.status.success() { - let error = String::from_utf8_lossy(&commit_output.stderr); - return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error))); - } - - // Pull the latest changes - let pull_output = Command::new("git") - .args(&["-C", actual_path, "pull"]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if pull_output.status.success() { - Ok(format!("Successfully committed and updated repository at {}", actual_path)) - } else { - let error = String::from_utf8_lossy(&pull_output.stderr); - Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) - } -} - -/** - * Commits changes in a git repository and pushes them to the remote. - * - * # Arguments - * - * * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository - * * `message` - The commit message - * - * # Returns - * - * * `Ok(String)` - A success message indicating the repository was committed and pushed - * * `Err(GitError)` - An error if the operation failed - * - * # Examples - * - * ``` - * let result = git_update_commit_push("my-project", "Add new feature")?; - * println!("{}", result); // "Successfully committed and pushed repository at /home/user/code/github.com/user/my-project" - * ``` - */ -pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result { - // If repo_path may be a partial path, find the matching repository - let repos = find_matching_repos(repo_path)?; - - // Should only be one repository at this point - let actual_path = &repos[0]; - - // Check if repository exists and is a git repository - let git_dir = Path::new(actual_path).join(".git"); - if !git_dir.exists() || !git_dir.is_dir() { - return Err(GitError::NotAGitRepository(actual_path.clone())); - } - - // Check for local changes - if !has_git_changes(actual_path)? { - return Ok(format!("No changes to commit in repository at {}", actual_path)); - } - - // Add all changes - let add_output = Command::new("git") - .args(&["-C", actual_path, "add", "."]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if !add_output.status.success() { - let error = String::from_utf8_lossy(&add_output.stderr); - return Err(GitError::GitCommandFailed(format!("Git add error: {}", error))); - } - - // Commit the changes - let commit_output = Command::new("git") - .args(&["-C", actual_path, "commit", "-m", message]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if !commit_output.status.success() { - let error = String::from_utf8_lossy(&commit_output.stderr); - return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error))); - } - - // Push the changes - let push_output = Command::new("git") - .args(&["-C", actual_path, "push"]) - .output() - .map_err(GitError::CommandExecutionError)?; - - if push_output.status.success() { - Ok(format!("Successfully committed and pushed repository at {}", actual_path)) - } else { - let error = String::from_utf8_lossy(&push_output.stderr); - Err(GitError::GitCommandFailed(format!("Git push error: {}", error))) +// Implement Clone for GitRepo to allow for method chaining +impl Clone for GitRepo { + fn clone(&self) -> Self { + GitRepo { + path: self.path.clone(), + } } } diff --git a/src/git/git_executor.rs b/src/git/git_executor.rs index 197426b..ac0ca18 100644 --- a/src/git/git_executor.rs +++ b/src/git/git_executor.rs @@ -160,7 +160,7 @@ impl GitExecutor { // Get authentication configuration for a git URL fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> { if let Some(config) = &self.config { - let (server, _, _) = parse_git_url(url); + let (server, _, _) = crate::git::git::parse_git_url(url); if !server.is_empty() { return config.auth.get(&server); } diff --git a/src/git_interface_redesign_plan.md b/src/git_interface_redesign_plan.md new file mode 100644 index 0000000..a24f81e --- /dev/null +++ b/src/git_interface_redesign_plan.md @@ -0,0 +1,212 @@ +# Git Interface Redesign Plan + +## Current Understanding + +The current git interface consists of standalone functions like `git_clone`, `git_list`, `git_update`, etc. We want to replace this with an object-oriented interface using a builder pattern that allows for method chaining. + +## New Interface Design + +### Core Components + +```mermaid +classDiagram + class GitTree { + +String base_path + +new(base_path: &str) Result + +list() Result, GitError> + +find(pattern: &str) Result, GitError> + +get(path_pattern: &str) Result, GitError> + } + + class GitRepo { + +String path + +pull() Result + +reset() Result + +push() Result + +commit(message: &str) Result + +has_changes() Result + } + + GitTree --> GitRepo : creates +``` + +### Implementation Details + +1. **GitTree Class**: + - Constructor takes a base path parameter that specifies where all git repositories will be located + - Methods for listing and finding repositories + - A `get()` method that returns one or more GitRepo objects based on a path pattern + - The `get()` method can also accept a URL (git or http format) and will clone the repository if it doesn't exist + +2. **GitRepo Class**: + - Represents a single git repository + - Methods for common git operations: pull, reset, push, commit + - Each method returns a Result containing either the GitRepo object (for chaining) or an error + - If an operation fails, subsequent operations in the chain are skipped + +3. **Error Handling**: + - Each method returns a Result type for immediate error handling + - Errors are propagated up the call chain + - The existing GitError enum will be reused + +## Implementation Plan + +### 1. Create the GitTree and GitRepo Structs in git.rs + +```rust +pub struct GitTree { + base_path: String, +} + +pub struct GitRepo { + path: String, +} +``` + +### 2. Implement the GitTree Methods + +```rust +impl GitTree { + pub fn new(base_path: &str) -> Result { + // Validate the base path + // Create the directory if it doesn't exist + Ok(GitTree { + base_path: base_path.to_string(), + }) + } + + pub fn list(&self) -> Result, GitError> { + // List all git repositories under the base path + } + + pub fn find(&self, pattern: &str) -> Result, GitError> { + // Find repositories matching the pattern + } + + pub fn get(&self, path_pattern: &str) -> Result, GitError> { + // Find repositories matching the pattern + // Return GitRepo objects for each match + } +} +``` + +### 3. Implement the GitRepo Methods + +```rust +impl GitRepo { + pub fn pull(&self) -> Result { + // Pull the latest changes + // Return self for chaining or an error + } + + pub fn reset(&self) -> Result { + // Reset any local changes + // Return self for chaining or an error + } + + pub fn push(&self) -> Result { + // Push changes to the remote + // Return self for chaining or an error + } + + pub fn commit(&self, message: &str) -> Result { + // Commit changes with the given message + // Return self for chaining or an error + } + + pub fn has_changes(&self) -> Result { + // Check if the repository has uncommitted changes + } +} +``` + +### 4. Update the Rhai Wrappers in rhai/git.rs + +```rust +// Register the GitTree and GitRepo types with Rhai +pub fn register_git_module(engine: &mut Engine) -> Result<(), Box> { + // Register the GitTree type + engine.register_type::(); + engine.register_fn("new", git_tree_new); + + // Register GitTree methods + engine.register_fn("list", git_tree_list); + engine.register_fn("find", git_tree_find); + engine.register_fn("get", git_tree_get); + + // Register GitRepo methods + engine.register_type::(); + engine.register_fn("pull", git_repo_pull); + engine.register_fn("reset", git_repo_reset); + engine.register_fn("push", git_repo_push); + engine.register_fn("commit", git_repo_commit); + engine.register_fn("has_changes", git_repo_has_changes); + + Ok(()) +} +``` + +### 5. Update Tests and Examples + +- Update the test files to use the new interface +- Create new examples demonstrating the builder pattern and method chaining + +## Usage Examples + +### Example 1: Basic Repository Operations + +```rhai +// Create a new GitTree object +let git_tree = new("/home/user/code"); + +// List all repositories +let repos = git_tree.list(); +print(`Found ${repos.len()} repositories`); + +// Find repositories matching a pattern +let matching = git_tree.find("my-project*"); +print(`Found ${matching.len()} matching repositories`); + +// Get a repository and perform operations +let repo = git_tree.get("my-project")[0]; +let result = repo.pull().reset().commit("Update files").push(); +``` + +### Example 2: Working with Multiple Repositories + +```rhai +// Create a new GitTree object +let git_tree = new("/home/user/code"); + +// Get all repositories matching a pattern +let repos = git_tree.get("project*"); +print(`Found ${repos.len()} matching repositories`); + +// Perform operations on all repositories +for repo in repos { + let result = repo.pull(); + if result.is_ok() { + print(`Successfully pulled ${repo.path}`); + } else { + print(`Failed to pull ${repo.path}: ${result.error}`); + } +} +``` + +### Example 3: Cloning a Repository + +```rhai +// Create a new GitTree object +let git_tree = new("/home/user/code"); + +// Clone a repository by URL +let repos = git_tree.get("https://github.com/username/repo.git"); +let repo = repos[0]; +print(`Repository cloned to: ${repo.path}`); +``` + +## Migration Strategy + +1. Implement the new interface in git.rs and rhai/git.rs +2. Update all tests and examples to use the new interface +3. Remove the old standalone functions \ No newline at end of file diff --git a/src/rhai/buildah.rs b/src/rhai/buildah.rs index 8aa9eb0..4e8087a 100644 --- a/src/rhai/buildah.rs +++ b/src/rhai/buildah.rs @@ -4,7 +4,7 @@ use rhai::{Engine, EvalAltResult, Array, Dynamic, Map}; use std::collections::HashMap; -use crate::virt::buildah::{self, BuildahError, Image}; +use crate::virt::buildah::{self, BuildahError, Image, Builder}; use crate::process::CommandResult; /// Register Buildah module functions with the Rhai engine @@ -20,35 +20,42 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box // Register types register_bah_types(engine)?; - // Register container functions - engine.register_fn("bah_from", bah_from); - engine.register_fn("bah_run", bah_run); - engine.register_fn("bah_run_with_isolation", bah_run_with_isolation); - engine.register_fn("bah_copy", bah_copy); - engine.register_fn("bah_add", bah_add); - engine.register_fn("bah_commit", bah_commit); - engine.register_fn("bah_remove", bah_remove); - engine.register_fn("bah_list", bah_list); - engine.register_fn("bah_build", bah_build_with_options); - engine.register_fn("bah_new_build_options", new_build_options); + // Register Builder constructor + engine.register_fn("bah_new", bah_new); - // Register image functions - engine.register_fn("bah_images", images); - engine.register_fn("bah_image_remove", image_remove); - engine.register_fn("bah_image_push", image_push); - engine.register_fn("bah_image_tag", image_tag); - engine.register_fn("bah_image_pull", image_pull); - engine.register_fn("bah_image_commit", image_commit_with_options); - engine.register_fn("bah_new_commit_options", new_commit_options); - engine.register_fn("bah_config", config_with_options); - engine.register_fn("bah_new_config_options", new_config_options); + // Register Builder instance methods + engine.register_fn("run", builder_run); + engine.register_fn("run_with_isolation", builder_run_with_isolation); + engine.register_fn("copy", builder_copy); + engine.register_fn("add", builder_add); + engine.register_fn("commit", builder_commit); + // Remove the line that's causing the error + engine.register_fn("remove", builder_remove); + engine.register_fn("reset", builder_reset); + engine.register_fn("config", builder_config); + + // Register Builder static methods + engine.register_fn("images", builder_images); + engine.register_fn("image_remove", builder_image_remove); + engine.register_fn("image_pull", builder_image_pull); + engine.register_fn("image_push", builder_image_push); + engine.register_fn("image_tag", builder_image_tag); + engine.register_fn("build", builder_build); Ok(()) } /// Register Buildah module types with the Rhai engine fn register_bah_types(engine: &mut Engine) -> Result<(), Box> { - // Register Image type and methods + // Register Builder type + engine.register_type_with_name::("BuildahBuilder"); + + // Register getters for Builder properties + engine.register_get("container_id", get_builder_container_id); + engine.register_get("name", get_builder_name); + engine.register_get("image", get_builder_image); + + // Register Image type and methods (same as before) engine.register_type_with_name::("BuildahImage"); // Register getters for Image properties @@ -84,312 +91,8 @@ fn bah_error_to_rhai_error(result: Result) -> Result 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 bah_from(image: &str) -> Result> { - let result = bah_error_to_rhai_error(buildah::from(image))?; - - // Create a new CommandResult with trimmed stdout - let trimmed_result = CommandResult { - stdout: result.stdout.trim().to_string(), - stderr: result.stderr.trim().to_string(), - success: result.success, - code: result.code, - }; - - Ok(trimmed_result) -} - -/// Wrapper for buildah::run -/// -/// Run a command in a container. -pub fn bah_run(container: &str, command: &str) -> Result> { - bah_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 bah_run_with_isolation(container: &str, command: &str, isolation: &str) -> Result> { - bah_error_to_rhai_error(buildah::bah_run_with_isolation(container, command, isolation)) -} - -/// Wrapper for buildah::copy -/// -/// Copy files into a container. -pub fn bah_copy(container: &str, source: &str, dest: &str) -> Result> { - bah_error_to_rhai_error(buildah::bah_copy(container, source, dest)) -} - -/// Wrapper for buildah::add -/// -/// Add files into a container. -pub fn bah_add(container: &str, source: &str, dest: &str) -> Result> { - bah_error_to_rhai_error(buildah::bah_add(container, source, dest)) -} - -/// Wrapper for buildah::commit -/// -/// Commit a container to an image. -pub fn bah_commit(container: &str, image_name: &str) -> Result> { - bah_error_to_rhai_error(buildah::bah_commit(container, image_name)) -} - -/// Wrapper for buildah::remove -/// -/// Remove a container. -pub fn bah_remove(container: &str) -> Result> { - bah_error_to_rhai_error(buildah::bah_remove(container)) -} - -/// Wrapper for buildah::list -/// -/// List containers. -pub fn bah_list() -> Result> { - bah_error_to_rhai_error(buildah::bah_list()) -} - -/// Build an image with options specified in a Map -/// -/// This provides a builder-style interface for Rhai scripts. -/// -/// # Example -/// -/// ```rhai -/// let options = bah_new_build_options(); -/// options.tag = "my-image:latest"; -/// options.context_dir = "."; -/// options.file = "Dockerfile"; -/// options.isolation = "chroot"; -/// let result = bah_build(options); -/// ``` -pub fn bah_build_with_options(options: Map) -> Result> { - // 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 - bah_error_to_rhai_error(buildah::bah_build(tag_ref, &context_dir, &file, isolation_ref)) -} - -// -// Image Function Wrappers -// - -/// Wrapper for buildah::images -/// -/// List images in local storage. -pub fn images() -> Result> { - let images = bah_error_to_rhai_error(buildah::images())?; - - // Convert Vec 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> { - bah_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> { - bah_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> { - bah_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> { - bah_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 = bah_new_commit_options(); -/// options.format = "docker"; -/// options.squash = true; -/// options.rm = true; -/// let result = bah_image_commit("my-container", "my-image:latest", options); -/// ``` -pub fn image_commit_with_options(container: &str, image_name: &str, options: Map) -> Result> { - // 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 - bah_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 = bah_new_config_options(); -/// options.author = "John Doe"; -/// options.cmd = "echo Hello"; -/// options.entrypoint = "/bin/sh -c"; -/// let result = bah_config("my-container", options); -/// ``` -pub fn config_with_options(container: &str, options: Map) -> Result> { - // Convert Rhai Map to Rust HashMap +// Helper function to convert Rhai Map to Rust HashMap +fn convert_map_to_hashmap(options: Map) -> Result, Box> { let mut config_options = HashMap::::new(); for (key, value) in options.iter() { @@ -404,6 +107,96 @@ pub fn config_with_options(container: &str, options: Map) -> Result Result> { + bah_error_to_rhai_error(Builder::new(name, image)) +} + +// Builder instance methods +pub fn builder_run(builder: &mut Builder, command: &str) -> Result> { + bah_error_to_rhai_error(builder.run(command)) +} + +pub fn builder_run_with_isolation(builder: &mut Builder, command: &str, isolation: &str) -> Result> { + bah_error_to_rhai_error(builder.run_with_isolation(command, isolation)) +} + +pub fn builder_copy(builder: &mut Builder, source: &str, dest: &str) -> Result> { + bah_error_to_rhai_error(builder.copy(source, dest)) +} + +pub fn builder_add(builder: &mut Builder, source: &str, dest: &str) -> Result> { + bah_error_to_rhai_error(builder.add(source, dest)) +} + +pub fn builder_commit(builder: &mut Builder, image_name: &str) -> Result> { + bah_error_to_rhai_error(builder.commit(image_name)) +} + +pub fn builder_remove(builder: &mut Builder) -> Result> { + bah_error_to_rhai_error(builder.remove()) +} + +pub fn builder_config(builder: &mut Builder, options: Map) -> Result> { + // Convert Rhai Map to Rust HashMap + let config_options = convert_map_to_hashmap(options)?; + bah_error_to_rhai_error(builder.config(config_options)) +} + +// Builder static methods +pub fn builder_images(_builder: &mut Builder) -> Result> { + let images = bah_error_to_rhai_error(Builder::images())?; + + // Convert Vec to Rhai Array + let mut array = Array::new(); + for image in images { + array.push(Dynamic::from(image)); + } + + Ok(array) +} + +pub fn builder_image_remove(_builder: &mut Builder, image: &str) -> Result> { + bah_error_to_rhai_error(Builder::image_remove(image)) +} + +pub fn builder_image_pull(_builder: &mut Builder, image: &str, tls_verify: bool) -> Result> { + bah_error_to_rhai_error(Builder::image_pull(image, tls_verify)) +} + +pub fn builder_image_push(_builder: &mut Builder, image: &str, destination: &str, tls_verify: bool) -> Result> { + bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify)) +} + +pub fn builder_image_tag(_builder: &mut Builder, image: &str, new_name: &str) -> Result> { + bah_error_to_rhai_error(Builder::image_tag(image, new_name)) +} + +// Getter functions for Builder properties +pub fn get_builder_container_id(builder: &mut Builder) -> String { + match builder.container_id() { + Some(id) => id.clone(), + None => "".to_string(), + } +} + +pub fn get_builder_name(builder: &mut Builder) -> String { + builder.name().to_string() +} + +pub fn get_builder_image(builder: &mut Builder) -> String { + builder.image().to_string() +} + +// Reset function for Builder +pub fn builder_reset(builder: &mut Builder) -> Result<(), Box> { + bah_error_to_rhai_error(builder.reset()) +} + +// Build function for Builder +pub fn builder_build(_builder: &mut Builder, tag: &str, context_dir: &str, file: &str, isolation: &str) -> Result> { + bah_error_to_rhai_error(Builder::build(Some(tag), context_dir, file, Some(isolation))) } \ No newline at end of file diff --git a/src/rhai/git.rs b/src/rhai/git.rs index 968ac9c..82bd4fd 100644 --- a/src/rhai/git.rs +++ b/src/rhai/git.rs @@ -3,7 +3,7 @@ //! This module provides Rhai wrappers for the functions in the Git module. use rhai::{Engine, EvalAltResult, Array, Dynamic}; -use crate::git::{self, GitError}; +use crate::git::{GitTree, GitRepo, GitError}; /// Register Git module functions with the Rhai engine /// @@ -15,15 +15,21 @@ use crate::git::{self, GitError}; /// /// * `Result<(), Box>` - Ok if registration was successful, Err otherwise pub fn register_git_module(engine: &mut Engine) -> Result<(), Box> { - // Register basic git functions - engine.register_fn("git_clone", git_clone); - engine.register_fn("git_list", git_list); - engine.register_fn("git_update", git_update); - engine.register_fn("git_update_force", git_update_force); - engine.register_fn("git_update_commit", git_update_commit); - engine.register_fn("git_update_commit_push", git_update_commit_push); - engine.register_fn("git_has_changes", has_git_changes); - engine.register_fn("git_find_repos", find_matching_repos); + // Register GitTree constructor + engine.register_fn("gittree_new", git_tree_new); + + // Register GitTree methods + engine.register_fn("list", git_tree_list); + engine.register_fn("find", git_tree_find); + engine.register_fn("get", git_tree_get); + + // Register GitRepo methods + engine.register_fn("path", git_repo_path); + engine.register_fn("has_changes", git_repo_has_changes); + engine.register_fn("pull", git_repo_pull); + engine.register_fn("reset", git_repo_reset); + engine.register_fn("commit", git_repo_commit); + engine.register_fn("push", git_repo_push); Ok(()) } @@ -39,21 +45,21 @@ fn git_error_to_rhai_error(result: Result) -> Result Result> { - git_error_to_rhai_error(git::git_clone(url)) +/// Creates a new GitTree with the specified base path. +pub fn git_tree_new(base_path: &str) -> Result> { + git_error_to_rhai_error(GitTree::new(base_path)) } -/// Wrapper for git::git_list +/// Wrapper for GitTree::list /// -/// Lists all git repositories found in the user's ~/code directory. -pub fn git_list() -> Result> { - let repos = git_error_to_rhai_error(git::git_list())?; +/// Lists all git repositories under the base path. +pub fn git_tree_list(git_tree: &mut GitTree) -> Result> { + let repos = git_error_to_rhai_error(git_tree.list())?; // Convert Vec to Rhai Array let mut array = Array::new(); @@ -64,18 +70,11 @@ pub fn git_list() -> Result> { Ok(array) } -/// Wrapper for git::has_git_changes -/// -/// Checks if a git repository has uncommitted changes. -pub fn has_git_changes(repo_path: &str) -> Result> { - git_error_to_rhai_error(git::has_git_changes(repo_path)) -} - -/// Wrapper for git::find_matching_repos +/// Wrapper for GitTree::find /// /// Finds repositories matching a pattern or partial path. -pub fn find_matching_repos(pattern: &str) -> Result> { - let repos = git_error_to_rhai_error(git::find_matching_repos(pattern))?; +pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result> { + let repos = git_error_to_rhai_error(git_tree.find(pattern))?; // Convert Vec to Rhai Array let mut array = Array::new(); @@ -86,30 +85,63 @@ pub fn find_matching_repos(pattern: &str) -> Result> { Ok(array) } -/// Wrapper for git::git_update +/// Wrapper for GitTree::get /// -/// Updates a git repository by pulling the latest changes. -pub fn git_update(repo_path: &str) -> Result> { - git_error_to_rhai_error(git::git_update(repo_path)) +/// Gets one or more GitRepo objects based on a path pattern or URL. +pub fn git_tree_get(git_tree: &mut GitTree, path_or_url: &str) -> Result> { + let repos = git_error_to_rhai_error(git_tree.get(path_or_url))?; + + // Convert Vec to Rhai Array + let mut array = Array::new(); + for repo in repos { + array.push(Dynamic::from(repo)); + } + + Ok(array) } -/// Wrapper for git::git_update_force +// +// GitRepo Function Wrappers +// + +/// Wrapper for GitRepo::path /// -/// Force updates a git repository by discarding local changes and pulling the latest changes. -pub fn git_update_force(repo_path: &str) -> Result> { - git_error_to_rhai_error(git::git_update_force(repo_path)) +/// Gets the path of the repository. +pub fn git_repo_path(git_repo: &mut GitRepo) -> String { + git_repo.path().to_string() } -/// Wrapper for git::git_update_commit +/// Wrapper for GitRepo::has_changes /// -/// Commits changes in a git repository and then updates it by pulling the latest changes. -pub fn git_update_commit(repo_path: &str, message: &str) -> Result> { - git_error_to_rhai_error(git::git_update_commit(repo_path, message)) +/// Checks if the repository has uncommitted changes. +pub fn git_repo_has_changes(git_repo: &mut GitRepo) -> Result> { + git_error_to_rhai_error(git_repo.has_changes()) } -/// Wrapper for git::git_update_commit_push +/// Wrapper for GitRepo::pull /// -/// Commits changes in a git repository and pushes them to the remote. -pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result> { - git_error_to_rhai_error(git::git_update_commit_push(repo_path, message)) +/// Pulls the latest changes from the remote repository. +pub fn git_repo_pull(git_repo: &mut GitRepo) -> Result> { + git_error_to_rhai_error(git_repo.pull()) +} + +/// Wrapper for GitRepo::reset +/// +/// Resets any local changes in the repository. +pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result> { + git_error_to_rhai_error(git_repo.reset()) +} + +/// Wrapper for GitRepo::commit +/// +/// Commits changes in the repository. +pub fn git_repo_commit(git_repo: &mut GitRepo, message: &str) -> Result> { + git_error_to_rhai_error(git_repo.commit(message)) +} + +/// Wrapper for GitRepo::push +/// +/// Pushes changes to the remote repository. +pub fn git_repo_push(git_repo: &mut GitRepo) -> Result> { + git_error_to_rhai_error(git_repo.push()) } \ No newline at end of file diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index 813bedc..a5de991 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -39,25 +39,22 @@ pub use process::{ // Re-export buildah functions pub use buildah::register_bah_module; -pub use buildah::{ - bah_from, bah_run, bah_run_with_isolation, bah_copy, bah_add, bah_commit, - bah_remove, bah_list, bah_build_with_options, - new_commit_options, new_config_options, image_commit_with_options, config_with_options -}; +pub use buildah::bah_new; // Re-export nerdctl functions pub use nerdctl::register_nerdctl_module; pub use nerdctl::{ - nerdctl_run, nerdctl_exec, - nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list + // Container functions + nerdctl_run, nerdctl_run_with_name, nerdctl_run_with_port, + nerdctl_exec, nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list, + // Image functions + nerdctl_images, nerdctl_image_remove, nerdctl_image_push, nerdctl_image_tag, + nerdctl_image_pull, nerdctl_image_commit, nerdctl_image_build }; -// Re-export git functions +// Re-export git module pub use git::register_git_module; -pub use git::{ - git_clone, git_list, git_update, git_update_force, git_update_commit, - git_update_commit_push, has_git_changes, find_matching_repos -}; +pub use crate::git::{GitTree, GitRepo}; // Rename copy functions to avoid conflicts pub use os::copy as os_copy; diff --git a/src/rhai/tests/git_test.rhai b/src/rhai/tests/git_test.rhai index 7eaf9c9..12a5f42 100644 --- a/src/rhai/tests/git_test.rhai +++ b/src/rhai/tests/git_test.rhai @@ -4,38 +4,28 @@ import "os" as os; import "process" as process; -// Test git_clone function -fn test_git_clone() { - // Use a public repository for testing - let repo_url = "https://github.com/rhaiscript/rhai.git"; +// Test GitTree creation +fn test_git_tree_creation() { + // Get home directory + let home_dir = env("HOME"); + let code_dir = `${home_dir}/code`; - // Clone the repository - print("Testing git_clone..."); - let result = git_clone(repo_url); + print("Testing GitTree creation..."); + let git_tree = gittree_new(code_dir); - // Print the result - print(`Clone result: ${result}`); - - // Verify the repository exists - if result.contains("already exists") { - print("Repository already exists, test passed"); - return true; - } - - // Check if the path exists - if exist(result) { - print("Repository cloned successfully, test passed"); - return true; - } - - print("Repository clone failed"); - return false; + print(`Created GitTree with base path: ${code_dir}`); + return true; } -// Test git_list function -fn test_git_list() { - print("Testing git_list..."); - let repos = git_list(); +// Test GitTree list method +fn test_git_tree_list() { + // Get home directory + let home_dir = env("HOME"); + let code_dir = `${home_dir}/code`; + + print("Testing GitTree list method..."); + let git_tree = gittree_new(code_dir); + let repos = git_tree.list(); print(`Found ${repos.len()} repositories`); @@ -45,44 +35,32 @@ fn test_git_list() { print(` - ${repos[i]}`); } - return repos.len() > 0; + return repos.len() >= 0; // Success even if no repos found } -// Test git_has_changes function -fn test_git_has_changes() { - print("Testing git_has_changes..."); +// Test GitTree find method +fn test_git_tree_find() { + // Get home directory + let home_dir = env("HOME"); + let code_dir = `${home_dir}/code`; + + print("Testing GitTree find method..."); + let git_tree = gittree_new(code_dir); + let repos = git_tree.list(); - // Get a repository from the list - let repos = git_list(); if repos.len() == 0 { print("No repositories found, skipping test"); return true; } - let repo = repos[0]; - let has_changes = git_has_changes(repo); - - print(`Repository ${repo} has changes: ${has_changes}`); - return true; -} - -// Test find_matching_repos function -fn test_find_matching_repos() { - print("Testing find_matching_repos..."); - - // Get all repositories with wildcard - let all_repos = git_list(); - if all_repos.len() == 0 { - print("No repositories found, skipping test"); - return true; - } - // Extract a part of the first repo name to search for - let repo_name = all_repos[0].split("/").last(); - let search_pattern = repo_name.substring(0, 3) + "*"; + let repo_path = repos[0]; + let parts = repo_path.split("/"); + let repo_name = parts[parts.len() - 1]; + let search_pattern = repo_name + "*"; print(`Searching for repositories matching pattern: ${search_pattern}`); - let matching = find_matching_repos(search_pattern); + let matching = git_tree.find(search_pattern); print(`Found ${matching.len()} matching repositories`); for repo in matching { @@ -92,13 +70,74 @@ fn test_find_matching_repos() { return matching.len() > 0; } +// Test GitTree get method +fn test_git_tree_get() { + // Get home directory + let home_dir = env("HOME"); + let code_dir = `${home_dir}/code`; + + print("Testing GitTree get method..."); + let git_tree = gittree_new(code_dir); + let repos = git_tree.list(); + + if repos.len() == 0 { + print("No repositories found, skipping test"); + return true; + } + + // Extract a part of the first repo name to search for + let repo_path = repos[0]; + let parts = repo_path.split("/"); + let repo_name = parts[parts.len() - 1]; + + print(`Getting GitRepo objects for: ${repo_name}`); + let git_repos = git_tree.get(repo_name); + + print(`Found ${git_repos.len()} GitRepo objects`); + for repo in git_repos { + print(` - ${repo.path()}`); + } + + return git_repos.len() > 0; +} + +// Test GitRepo has_changes method +fn test_git_repo_has_changes() { + // Get home directory + let home_dir = env("HOME"); + let code_dir = `${home_dir}/code`; + + print("Testing GitRepo has_changes method..."); + let git_tree = gittree_new(code_dir); + let repos = git_tree.list(); + + if repos.len() == 0 { + print("No repositories found, skipping test"); + return true; + } + + // Get the first repo + let git_repos = git_tree.get(repos[0]); + if git_repos.len() == 0 { + print("Failed to get GitRepo object, skipping test"); + return true; + } + + let git_repo = git_repos[0]; + let has_changes = git_repo.has_changes(); + + print(`Repository ${git_repo.path()} has changes: ${has_changes}`); + return true; +} + // Run the tests fn run_tests() { let tests = [ - #{ name: "git_clone", fn: test_git_clone }, - #{ name: "git_list", fn: test_git_list }, - #{ name: "git_has_changes", fn: test_git_has_changes }, - #{ name: "find_matching_repos", fn: test_find_matching_repos } + #{ name: "git_tree_creation", fn: test_git_tree_creation }, + #{ name: "git_tree_list", fn: test_git_tree_list }, + #{ name: "git_tree_find", fn: test_git_tree_find }, + #{ name: "git_tree_get", fn: test_git_tree_get }, + #{ name: "git_repo_has_changes", fn: test_git_repo_has_changes } ]; let passed = 0; diff --git a/src/simple_git_test.rhai b/src/simple_git_test.rhai new file mode 100644 index 0000000..c1fe1f8 --- /dev/null +++ b/src/simple_git_test.rhai @@ -0,0 +1,11 @@ +// Simple test script for Git module functions + +// Print a header +print("=== Testing Git Module Functions ===\n"); + +// Create a new GitTree object +let home_dir = env("HOME"); +let git_tree = gittree_new(`${home_dir}/code`); +print(`Created GitTree with base path: ${home_dir}/code`); + +print("\n=== Git Module Test Complete ==="); \ No newline at end of file diff --git a/src/test_git.rhai b/src/test_git.rhai index a5f556a..727a4df 100644 --- a/src/test_git.rhai +++ b/src/test_git.rhai @@ -3,9 +3,14 @@ // Print a header print("=== Testing Git Module Functions ===\n"); -// Test git_list function -print("Listing git repositories..."); -let repos = git_list(); +// Create a new GitTree object +let home_dir = env("HOME"); +let git_tree = gittree_new(`${home_dir}/code`); +print(`Created GitTree with base path: ${home_dir}/code`); + +// Test list method +print("\nListing git repositories..."); +let repos = git_tree.list(); print(`Found ${repos.len()} repositories`); // Print the first few repositories @@ -17,7 +22,7 @@ if repos.len() > 0 { } } -// Test find_matching_repos function +// Test find method if repos.len() > 0 { print("\nTesting repository search..."); // Extract a part of the first repo name to search for @@ -26,17 +31,35 @@ if repos.len() > 0 { let repo_name = parts[parts.len() - 1]; print(`Searching for repositories containing "${repo_name}"`); - let matching = find_matching_repos(repo_name); + let matching = git_tree.find(repo_name); print(`Found ${matching.len()} matching repositories`); for repo in matching { print(` - ${repo}`); } - // Check if a repository has changes - print("\nChecking for changes in repository..."); - let has_changes = git_has_changes(repo_path); - print(`Repository ${repo_path} has changes: ${has_changes}`); + // Test get method + print("\nTesting get method..."); + let git_repos = git_tree.get(repo_name); + print(`Found ${git_repos.len()} GitRepo objects`); + + // Test GitRepo methods + if git_repos.len() > 0 { + let git_repo = git_repos[0]; + print(`\nTesting GitRepo methods on: ${git_repo.path()}`); + + // Check if a repository has changes + print("Checking for changes in repository..."); + let has_changes = git_repo.has_changes(); + print(`Repository has changes: ${has_changes}`); + + // Test method chaining (only if there are no changes to avoid errors) + if !has_changes { + print("\nTesting method chaining (pull)..."); + let result = git_repo.pull(); + print("Pull operation completed successfully"); + } + } } print("\n=== Git Module Test Complete ==="); \ No newline at end of file diff --git a/src/virt/buildah/builder.rs b/src/virt/buildah/builder.rs new file mode 100644 index 0000000..9eb3cbb --- /dev/null +++ b/src/virt/buildah/builder.rs @@ -0,0 +1,458 @@ +use crate::process::CommandResult; +use crate::virt::buildah::{execute_buildah_command, BuildahError, Image}; +use std::collections::HashMap; + +/// Builder struct for buildah operations +#[derive(Clone)] +pub struct Builder { + /// Name of the container + name: String, + /// Container ID + container_id: Option, + /// Base image + image: String, +} + +impl Builder { + /// Create a new builder with a container from the specified image + /// + /// # Arguments + /// + /// * `name` - Name for the container + /// * `image` - Image to create the container from + /// + /// # Returns + /// + /// * `Result` - Builder instance or error + pub fn new(name: &str, image: &str) -> Result { + // Try to create a new container + let result = execute_buildah_command(&["from", "--name", name, image]); + + match result { + Ok(success_result) => { + // Container created successfully + let container_id = success_result.stdout.trim().to_string(); + + Ok(Self { + name: name.to_string(), + container_id: Some(container_id), + image: image.to_string(), + }) + }, + Err(BuildahError::CommandFailed(error_msg)) => { + // Check if the error is because the container already exists + if error_msg.contains("that name is already in use") { + // Extract the container ID from the error message + // Error format: "the container name "name" is already in use by container_id. You have to remove that container to be able to reuse that name: that name is already in use" + let container_id = error_msg + .split("already in use by ") + .nth(1) + .and_then(|s| s.split('.').next()) + .unwrap_or("") + .trim() + .to_string(); + + if !container_id.is_empty() { + // Container already exists, continue with it + Ok(Self { + name: name.to_string(), + container_id: Some(container_id), + image: image.to_string(), + }) + } else { + // Couldn't extract container ID + Err(BuildahError::Other("Failed to extract container ID from error message".to_string())) + } + } else { + // Other command failure + Err(BuildahError::CommandFailed(error_msg)) + } + }, + Err(e) => { + // Other error + Err(e) + } + } + } + + /// Get the container ID + pub fn container_id(&self) -> Option<&String> { + self.container_id.as_ref() + } + + /// Get the container name + pub fn name(&self) -> &str { + &self.name + } + + /// Get the base image + pub fn image(&self) -> &str { + &self.image + } + + /// Run a command in the container + /// + /// # Arguments + /// + /// * `command` - The command to run + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn run(&self, command: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["run", container_id, "sh", "-c", command]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + /// Run a command in the container with specified isolation + /// + /// # Arguments + /// + /// * `command` - The command to run + /// * `isolation` - Isolation method (e.g., "chroot", "rootless", "oci") + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn run_with_isolation(&self, command: &str, isolation: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["run", "--isolation", isolation, container_id, "sh", "-c", command]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + /// Copy files into the container + /// + /// # Arguments + /// + /// * `source` - Source path + /// * `dest` - Destination path in the container + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn copy(&self, source: &str, dest: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["copy", container_id, source, dest]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + /// Add files into the container + /// + /// # Arguments + /// + /// * `source` - Source path + /// * `dest` - Destination path in the container + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn add(&self, source: &str, dest: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["add", container_id, source, dest]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + /// Commit the container to an image + /// + /// # Arguments + /// + /// * `image_name` - Name for the new image + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn commit(&self, image_name: &str) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["commit", container_id, image_name]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + /// Remove the container + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn remove(&self) -> Result { + if let Some(container_id) = &self.container_id { + execute_buildah_command(&["rm", container_id]) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + /// Reset the builder by removing the container and clearing the container_id + /// + /// # Returns + /// + /// * `Result<(), BuildahError>` - Success or error + pub fn reset(&mut self) -> Result<(), BuildahError> { + if let Some(container_id) = &self.container_id { + // Try to remove the container + let result = execute_buildah_command(&["rm", container_id]); + + // Clear the container_id regardless of whether the removal succeeded + self.container_id = None; + + // Return the result of the removal operation + match result { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } else { + // No container to remove + Ok(()) + } + } + + /// Configure container metadata + /// + /// # Arguments + /// + /// * `options` - Map of configuration options + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn config(&self, options: HashMap) -> Result { + if let Some(container_id) = &self.container_id { + let mut args_owned: Vec = Vec::new(); + args_owned.push("config".to_string()); + + // Process options map + for (key, value) in options.iter() { + let option_name = format!("--{}", key); + args_owned.push(option_name); + args_owned.push(value.clone()); + } + + args_owned.push(container_id.clone()); + + // Convert Vec to Vec<&str> for execute_buildah_command + let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect(); + + execute_buildah_command(&args) + } else { + Err(BuildahError::Other("No container ID available".to_string())) + } + } + + /// List images in local storage + /// + /// # Returns + /// + /// * `Result, BuildahError>` - List of images or error + pub fn images() -> Result, BuildahError> { + let result = execute_buildah_command(&["images", "--json"])?; + + // Try to parse the JSON output + match serde_json::from_str::(&result.stdout) { + Ok(json) => { + if let serde_json::Value::Array(images_json) = json { + let mut images = Vec::new(); + + for image_json in images_json { + // Extract image ID + let id = match image_json.get("id").and_then(|v| v.as_str()) { + Some(id) => id.to_string(), + None => return Err(BuildahError::ConversionError("Missing image ID".to_string())), + }; + + // Extract image names + let names = match image_json.get("names").and_then(|v| v.as_array()) { + Some(names_array) => { + let mut names_vec = Vec::new(); + for name_value in names_array { + if let Some(name_str) = name_value.as_str() { + names_vec.push(name_str.to_string()); + } + } + names_vec + }, + None => Vec::new(), // Empty vector if no names found + }; + + // Extract image size + let size = match image_json.get("size").and_then(|v| v.as_str()) { + Some(size) => size.to_string(), + None => "Unknown".to_string(), // Default value if size not found + }; + + // Extract creation timestamp + let created = match image_json.get("created").and_then(|v| v.as_str()) { + Some(created) => created.to_string(), + None => "Unknown".to_string(), // Default value if created not found + }; + + // Create Image struct and add to vector + images.push(Image { + id, + names, + size, + created, + }); + } + + Ok(images) + } else { + Err(BuildahError::JsonParseError("Expected JSON array".to_string())) + } + }, + Err(e) => { + Err(BuildahError::JsonParseError(format!("Failed to parse image list JSON: {}", e))) + } + } + } + + /// Remove an image + /// + /// # Arguments + /// + /// * `image` - Image ID or name + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn image_remove(image: &str) -> Result { + execute_buildah_command(&["rmi", image]) + } + + /// Pull an image from a registry + /// + /// # Arguments + /// + /// * `image` - Image name + /// * `tls_verify` - Whether to verify TLS + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn image_pull(image: &str, tls_verify: bool) -> Result { + let mut args = vec!["pull"]; + + if !tls_verify { + args.push("--tls-verify=false"); + } + + args.push(image); + + execute_buildah_command(&args) + } + + /// Push an image to a registry + /// + /// # Arguments + /// + /// * `image` - Image name + /// * `destination` - Destination registry + /// * `tls_verify` - Whether to verify TLS + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result { + let mut args = vec!["push"]; + + if !tls_verify { + args.push("--tls-verify=false"); + } + + args.push(image); + args.push(destination); + + execute_buildah_command(&args) + } + + /// Tag an image + /// + /// # Arguments + /// + /// * `image` - Image ID or name + /// * `new_name` - New tag for the image + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn image_tag(image: &str, new_name: &str) -> Result { + execute_buildah_command(&["tag", image, new_name]) + } + + /// Commit a container to an image with advanced options + /// + /// # Arguments + /// + /// * `container` - Container ID or name + /// * `image_name` - Name for the new image + /// * `format` - Optional format (oci or docker) + /// * `squash` - Whether to squash layers + /// * `rm` - Whether to remove the container after commit + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn image_commit(container: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result { + let mut args = vec!["commit"]; + + if let Some(format_str) = format { + args.push("--format"); + args.push(format_str); + } + + if squash { + args.push("--squash"); + } + + if rm { + args.push("--rm"); + } + + args.push(container); + args.push(image_name); + + execute_buildah_command(&args) + } + + /// Build an image from a Containerfile/Dockerfile + /// + /// # Arguments + /// + /// * `tag` - Optional tag for the image + /// * `context_dir` - Directory containing the Containerfile/Dockerfile + /// * `file` - Path to the Containerfile/Dockerfile + /// * `isolation` - Optional isolation method + /// + /// # Returns + /// + /// * `Result` - Command result or error + pub fn build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result { + let mut args = Vec::new(); + args.push("build"); + + if let Some(tag_value) = tag { + args.push("-t"); + args.push(tag_value); + } + + if let Some(isolation_value) = isolation { + args.push("--isolation"); + args.push(isolation_value); + } + + args.push("-f"); + args.push(file); + + args.push(context_dir); + + execute_buildah_command(&args) + } +} \ No newline at end of file diff --git a/src/virt/buildah/containers_test.rs b/src/virt/buildah/containers_test.rs index ac26522..f9f860e 100644 --- a/src/virt/buildah/containers_test.rs +++ b/src/virt/buildah/containers_test.rs @@ -11,6 +11,7 @@ mod tests { lazy_static! { static ref LAST_COMMAND: Mutex> = Mutex::new(Vec::new()); static ref SHOULD_FAIL: Mutex = Mutex::new(false); + static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); // Add a mutex for test synchronization } fn reset_test_state() { @@ -117,6 +118,7 @@ mod tests { // Tests for each function #[test] fn test_from_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); let image = "alpine:latest"; @@ -129,6 +131,7 @@ mod tests { #[test] fn test_run_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); let container = "my-container"; @@ -143,6 +146,7 @@ mod tests { #[test] fn test_bah_run_with_isolation_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); let container = "my-container"; @@ -157,6 +161,7 @@ mod tests { #[test] fn test_bah_copy_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); let container = "my-container"; @@ -171,6 +176,7 @@ mod tests { #[test] fn test_bah_add_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); let container = "my-container"; @@ -185,6 +191,7 @@ mod tests { #[test] fn test_bah_commit_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); let container = "my-container"; @@ -198,6 +205,7 @@ mod tests { #[test] fn test_bah_remove_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); let container = "my-container"; @@ -210,6 +218,7 @@ mod tests { #[test] fn test_bah_list_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); let result = test_bah_list(); @@ -221,6 +230,7 @@ mod tests { #[test] fn test_bah_build_function() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); // Test with tag, context directory, file, and no isolation @@ -229,12 +239,16 @@ mod tests { let cmd = get_last_command(); assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "-f", "Dockerfile", "."]); + reset_test_state(); // Reset state between sub-tests + // Test with tag, context directory, file, and isolation let result = test_bah_build(Some("my-app:latest"), ".", "Dockerfile.custom", Some("chroot")); assert!(result.is_ok()); let cmd = get_last_command(); assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "--isolation", "chroot", "-f", "Dockerfile.custom", "."]); + reset_test_state(); // Reset state between sub-tests + // Test with just context directory and file let result = test_bah_build(None, ".", "Dockerfile", None); assert!(result.is_ok()); @@ -244,6 +258,7 @@ mod tests { #[test] fn test_error_handling() { + let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test reset_test_state(); set_should_fail(true); diff --git a/src/virt/buildah/mod.rs b/src/virt/buildah/mod.rs index 11df9ae..4bf641b 100644 --- a/src/virt/buildah/mod.rs +++ b/src/virt/buildah/mod.rs @@ -1,6 +1,7 @@ mod containers; mod images; mod cmd; +mod builder; #[cfg(test)] mod containers_test; @@ -43,6 +44,12 @@ impl Error for BuildahError { } } } +// Re-export the Builder +pub use builder::Builder; + +// Re-export existing functions for backward compatibility +#[deprecated(since = "0.2.0", note = "Use Builder::new() instead")] pub use containers::*; +#[deprecated(since = "0.2.0", note = "Use Builder methods instead")] pub use images::*; pub use cmd::*; \ No newline at end of file