# 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}`);