...
This commit is contained in:
		| @@ -1,752 +0,0 @@ | ||||
| # 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<Builder, BuildahError> | ||||
|         +run(command: String) -> Result<CommandResult, BuildahError> | ||||
|         +run_with_isolation(command: String, isolation: String) -> Result<CommandResult, BuildahError> | ||||
|         +add(source: String, dest: String) -> Result<CommandResult, BuildahError> | ||||
|         +copy(source: String, dest: String) -> Result<CommandResult, BuildahError> | ||||
|         +commit(image_name: String) -> Result<CommandResult, BuildahError> | ||||
|         +remove() -> Result<CommandResult, BuildahError> | ||||
|         +config(options: HashMap<String, String>) -> Result<CommandResult, BuildahError> | ||||
|     } | ||||
|      | ||||
|     class BuilderStatic { | ||||
|         +images() -> Result<Vec<Image>, BuildahError> | ||||
|         +image_remove(image: String) -> Result<CommandResult, BuildahError> | ||||
|         +image_pull(image: String, tls_verify: bool) -> Result<CommandResult, BuildahError> | ||||
|         +image_push(image: String, destination: String, tls_verify: bool) -> Result<CommandResult, BuildahError> | ||||
|         +image_tag(image: String, new_name: String) -> Result<CommandResult, BuildahError> | ||||
|         +image_commit(container: String, image_name: String, format: Option<String>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError> | ||||
|         +build(tag: Option<String>, context_dir: String, file: String, isolation: Option<String>) -> Result<CommandResult, BuildahError> | ||||
|     } | ||||
|      | ||||
|     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<String>, | ||||
|     image: String, | ||||
| } | ||||
|  | ||||
| impl Builder { | ||||
|     pub fn new(name: &str, image: &str) -> Result<Self, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         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<String, String>) -> Result<CommandResult, BuildahError> { | ||||
|         if let Some(container_id) = &self.container_id { | ||||
|             let mut args_owned: Vec<String> = 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<String> 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<Vec<Image>, BuildahError> { | ||||
|         // Implementation from current images() function | ||||
|         let result = execute_buildah_command(&["images", "--json"])?; | ||||
|          | ||||
|         // Try to parse the JSON output | ||||
|         match serde_json::from_str::<serde_json::Value>(&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<CommandResult, BuildahError> { | ||||
|         execute_buildah_command(&["rmi", image]) | ||||
|     } | ||||
|      | ||||
|     pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         execute_buildah_command(&["tag", image, new_name]) | ||||
|     } | ||||
|      | ||||
|     pub fn image_commit(container: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError> { | ||||
|         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<CommandResult, BuildahError> { | ||||
|         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<EvalAltResult>>` - Ok if registration was successful, Err otherwise | ||||
| pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
|     // 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<CommandResult, Box<EvalAltResult>> { | ||||
|         bah_error_to_rhai_error(builder.run(command)) | ||||
|     }); | ||||
|      | ||||
|     engine.register_fn("run_with_isolation", |builder: &mut Builder, command: &str, isolation: &str| -> Result<CommandResult, Box<EvalAltResult>> { | ||||
|         bah_error_to_rhai_error(builder.run_with_isolation(command, isolation)) | ||||
|     }); | ||||
|      | ||||
|     engine.register_fn("copy", |builder: &mut Builder, source: &str, dest: &str| -> Result<CommandResult, Box<EvalAltResult>> { | ||||
|         bah_error_to_rhai_error(builder.copy(source, dest)) | ||||
|     }); | ||||
|      | ||||
|     engine.register_fn("add", |builder: &mut Builder, source: &str, dest: &str| -> Result<CommandResult, Box<EvalAltResult>> { | ||||
|         bah_error_to_rhai_error(builder.add(source, dest)) | ||||
|     }); | ||||
|      | ||||
|     engine.register_fn("commit", |builder: &mut Builder, image_name: &str| -> Result<CommandResult, Box<EvalAltResult>> { | ||||
|         bah_error_to_rhai_error(builder.commit(image_name)) | ||||
|     }); | ||||
|      | ||||
|     engine.register_fn("remove", |builder: &mut Builder| -> Result<CommandResult, Box<EvalAltResult>> { | ||||
|         bah_error_to_rhai_error(builder.remove()) | ||||
|     }); | ||||
|      | ||||
|     engine.register_fn("config", |builder: &mut Builder, options: Map| -> Result<CommandResult, Box<EvalAltResult>> { | ||||
|         // 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<Array, Box<EvalAltResult>> { | ||||
|         let images = bah_error_to_rhai_error(Builder::images())?; | ||||
|          | ||||
|         // Convert Vec<Image> 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<CommandResult, Box<EvalAltResult>> { | ||||
|         bah_error_to_rhai_error(Builder::image_remove(image)) | ||||
|     }); | ||||
|      | ||||
|     engine.register_fn("image_pull", |_: &mut Builder, image: &str, tls_verify: bool| -> Result<CommandResult, Box<EvalAltResult>> { | ||||
|         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<CommandResult, Box<EvalAltResult>> { | ||||
|         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<CommandResult, Box<EvalAltResult>> { | ||||
|         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<EvalAltResult>> { | ||||
|     // Register Builder type | ||||
|     engine.register_type_with_name::<Builder>("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::<Image>("BuildahImage"); | ||||
|      | ||||
|     // Register getters for Image properties | ||||
|     engine.register_get("id", |img: &mut Image| img.id.clone()); | ||||
|     engine.register_get("names", |img: &mut Image| { | ||||
|         let mut array = Array::new(); | ||||
|         for name in &img.names { | ||||
|             array.push(Dynamic::from(name.clone())); | ||||
|         } | ||||
|         array | ||||
|     }); | ||||
|     // Add a 'name' getter that returns the first name or a default | ||||
|     engine.register_get("name", |img: &mut Image| { | ||||
|         if img.names.is_empty() { | ||||
|             "<none>".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<EvalAltResult>> { | ||||
|     // 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<T>(result: Result<T, BuildahError>) -> Result<T, Box<EvalAltResult>> { | ||||
|     result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Buildah error: {}", e).into(), | ||||
|             rhai::Position::NONE | ||||
|         )) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| // Helper function to convert Rhai Map to Rust HashMap | ||||
| fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<EvalAltResult>> { | ||||
|     let mut config_options = HashMap::<String, String>::new(); | ||||
|      | ||||
|     for (key, value) in options.iter() { | ||||
|         if let Ok(value_str) = value.clone().into_string() { | ||||
|             // Convert SmartString to String | ||||
|             config_options.insert(key.to_string(), value_str); | ||||
|         } else { | ||||
|             return Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|                 format!("Option '{}' must be a string", key).into(), | ||||
|                 rhai::Position::NONE | ||||
|             ))); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     Ok(config_options) | ||||
| } | ||||
|  | ||||
| /// Create a new Builder | ||||
| pub fn bah_new(name: &str, image: &str) -> Result<Builder, Box<EvalAltResult>> { | ||||
|     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}`); | ||||
|  | ||||
| @@ -1,238 +0,0 @@ | ||||
| # Container Builder Implementation Plan | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| This document outlines the plan for redesigning the nerdctl interface in the `src/virt/nerdctl` directory to use an object-oriented approach with a Container struct that supports method chaining for the builder pattern. This will replace the existing function-based approach while maintaining all current functionality. | ||||
|  | ||||
| ## Architecture | ||||
|  | ||||
| ```mermaid | ||||
| classDiagram | ||||
|     class Container { | ||||
|         -String name | ||||
|         -String container_id | ||||
|         -String? image | ||||
|         -HashMap~String, String~ config | ||||
|         -Vec~String~ ports | ||||
|         -Vec~String~ volumes | ||||
|         -HashMap~String, String~ env_vars | ||||
|         -Option~String~ network | ||||
|         -Vec~String~ network_aliases | ||||
|         -Option~String~ cpu_limit | ||||
|         -Option~String~ memory_limit | ||||
|         -Option~String~ memory_swap_limit | ||||
|         -Option~String~ cpu_shares | ||||
|         -Option~String~ restart_policy | ||||
|         -Option~HealthCheck~ health_check | ||||
|         +new(name: &str) -> Result~Container, NerdctlError~ | ||||
|         +from_image(name: &str, image: &str) -> Result~Container, NerdctlError~ | ||||
|         +with_port(port: &str) -> Container | ||||
|         +with_ports(ports: &[&str]) -> Container | ||||
|         +with_volume(volume: &str) -> Container | ||||
|         +with_volumes(volumes: &[&str]) -> Container | ||||
|         +with_env(key: &str, value: &str) -> Container | ||||
|         +with_envs(env_map: &HashMap<&str, &str>) -> Container | ||||
|         +with_network(network: &str) -> Container | ||||
|         +with_network_alias(alias: &str) -> Container | ||||
|         +with_network_aliases(aliases: &[&str]) -> Container | ||||
|         +with_cpu_limit(cpus: &str) -> Container | ||||
|         +with_memory_limit(memory: &str) -> Container | ||||
|         +with_memory_swap_limit(memory_swap: &str) -> Container | ||||
|         +with_cpu_shares(shares: &str) -> Container | ||||
|         +with_restart_policy(policy: &str) -> Container | ||||
|         +with_health_check(cmd: &str) -> Container | ||||
|         +with_health_check_options(cmd: &str, interval: Option<&str>, timeout: Option<&str>, retries: Option<u32>, start_period: Option<&str>) -> Container | ||||
|         +with_snapshotter(snapshotter: &str) -> Container | ||||
|         +with_detach(detach: bool) -> Container | ||||
|         +build() -> Result~Container, NerdctlError~ | ||||
|         +start() -> Result~CommandResult, NerdctlError~ | ||||
|         +stop() -> Result~CommandResult, NerdctlError~ | ||||
|         +remove() -> Result~CommandResult, NerdctlError~ | ||||
|         +exec(command: &str) -> Result~CommandResult, NerdctlError~ | ||||
|         +copy(source: &str, dest: &str) -> Result~CommandResult, NerdctlError~ | ||||
|         +export(path: &str) -> Result~CommandResult, NerdctlError~ | ||||
|         +commit(image_name: &str) -> Result~CommandResult, NerdctlError~ | ||||
|         +status() -> Result~ContainerStatus, NerdctlError~ | ||||
|         +health_status() -> Result~String, NerdctlError~ | ||||
|         +resources() -> Result~ResourceUsage, NerdctlError~ | ||||
|     } | ||||
|      | ||||
|     class HealthCheck { | ||||
|         +String cmd | ||||
|         +Option~String~ interval | ||||
|         +Option~String~ timeout | ||||
|         +Option~u32~ retries | ||||
|         +Option~String~ start_period | ||||
|     } | ||||
|      | ||||
|     class ContainerStatus { | ||||
|         +String state | ||||
|         +String status | ||||
|         +String created | ||||
|         +String started | ||||
|         +Option~String~ health_status | ||||
|         +Option~String~ health_output | ||||
|     } | ||||
|      | ||||
|     class ResourceUsage { | ||||
|         +String cpu_usage | ||||
|         +String memory_usage | ||||
|         +String memory_limit | ||||
|         +String memory_percentage | ||||
|         +String network_input | ||||
|         +String network_output | ||||
|         +String block_input | ||||
|         +String block_output | ||||
|         +String pids | ||||
|     } | ||||
|      | ||||
|     class NerdctlError { | ||||
|         +CommandExecutionFailed(io::Error) | ||||
|         +CommandFailed(String) | ||||
|         +JsonParseError(String) | ||||
|         +ConversionError(String) | ||||
|         +Other(String) | ||||
|     } | ||||
|      | ||||
|     Container --> ContainerStatus : returns | ||||
|     Container --> ResourceUsage : returns | ||||
|     Container --> HealthCheck : contains | ||||
|     Container --> NerdctlError : may throw | ||||
| ``` | ||||
|  | ||||
| ## Implementation Steps | ||||
|  | ||||
| ### 1. Create Container Struct and Basic Methods | ||||
|  | ||||
| Create a new file `src/virt/nerdctl/container.rs` with the Container struct and basic methods. | ||||
|  | ||||
| ### 2. Implement Builder Pattern Methods | ||||
|  | ||||
| Add builder pattern methods to the Container struct for configuration. | ||||
|  | ||||
| ### 3. Implement Container Operations | ||||
|  | ||||
| Add methods for container operations like start, stop, exec, etc. | ||||
|  | ||||
| ### 4. Implement Status and Resource Usage Methods | ||||
|  | ||||
| Add methods for getting container status and resource usage information. | ||||
|  | ||||
| ### 5. Update mod.rs to Export the New Container Struct | ||||
|  | ||||
| Update `src/virt/nerdctl/mod.rs` to include the new container module. | ||||
|  | ||||
| ### 6. Create Example Usage | ||||
|  | ||||
| Create an example file to demonstrate the new Container API. | ||||
|  | ||||
| ## Key Features | ||||
|  | ||||
| ### Container Creation and Configuration | ||||
|  | ||||
| - Method chaining for the builder pattern | ||||
| - Support for multiple ports and volumes | ||||
| - Environment variable configuration | ||||
| - Network configuration and aliases | ||||
| - Resource limits (CPU, memory) | ||||
| - Restart policies | ||||
| - Health checks | ||||
|  | ||||
| ### Container Operations | ||||
|  | ||||
| - Start, stop, and remove containers | ||||
| - Execute commands in containers | ||||
| - Copy files between container and host | ||||
| - Export containers to tarballs | ||||
| - Commit containers to images | ||||
|  | ||||
| ### Container Monitoring | ||||
|  | ||||
| - Get container status information | ||||
| - Get container health status | ||||
| - Get resource usage information | ||||
|  | ||||
| ## Example Usage | ||||
|  | ||||
| ```rust | ||||
| // Create a container with various configuration options | ||||
| let container = Container::from_image("my-web-app", "nginx:latest")? | ||||
|     .with_ports(&["8080:80", "8443:443"]) | ||||
|     .with_volumes(&[ | ||||
|         "./html:/usr/share/nginx/html", | ||||
|         "./config/nginx.conf:/etc/nginx/nginx.conf" | ||||
|     ]) | ||||
|     .with_env("NGINX_HOST", "example.com") | ||||
|     .with_env("NGINX_PORT", "80") | ||||
|     .with_network("app-network") | ||||
|     .with_network_alias("web") | ||||
|     .with_cpu_limit("0.5") | ||||
|     .with_memory_limit("512m") | ||||
|     .with_restart_policy("always") | ||||
|     .with_health_check_options( | ||||
|         "curl -f http://localhost/ || exit 1", | ||||
|         Some("10s"), | ||||
|         Some("5s"), | ||||
|         Some(3), | ||||
|         Some("30s") | ||||
|     ) | ||||
|     .with_detach(true) | ||||
|     .build()?; | ||||
|  | ||||
| // Start the container | ||||
| container.start()?; | ||||
|  | ||||
| // Execute a command in the container | ||||
| let result = container.exec("echo 'Hello from container'")?; | ||||
| println!("Command output: {}", result.stdout); | ||||
|  | ||||
| // Get container status | ||||
| let status = container.status()?; | ||||
| println!("Container state: {}", status.state); | ||||
| println!("Container status: {}", status.status); | ||||
|  | ||||
| // Get resource usage | ||||
| let resources = container.resources()?; | ||||
| println!("CPU usage: {}", resources.cpu_usage); | ||||
| println!("Memory usage: {}", resources.memory_usage); | ||||
|  | ||||
| // Stop and remove the container | ||||
| container.stop()?; | ||||
| container.remove()?; | ||||
| ``` | ||||
|  | ||||
| ## Network Management | ||||
|  | ||||
| ```rust | ||||
| // Create a network | ||||
| Container::create_network("app-network", Some("bridge"))?; | ||||
|  | ||||
| // Create containers in the network | ||||
| let db = Container::from_image("db", "postgres:latest")? | ||||
|     .with_network("app-network") | ||||
|     .with_network_alias("database") | ||||
|     .with_env("POSTGRES_PASSWORD", "example") | ||||
|     .build()?; | ||||
|  | ||||
| let app = Container::from_image("app", "my-app:latest")? | ||||
|     .with_network("app-network") | ||||
|     .with_env("DATABASE_URL", "postgres://postgres:example@database:5432/postgres") | ||||
|     .build()?; | ||||
|  | ||||
| // Remove the network when done | ||||
| Container::remove_network("app-network")?; | ||||
| ``` | ||||
|  | ||||
| ## Migration Strategy | ||||
|  | ||||
| 1. Create the new Container struct and its methods | ||||
| 2. Update the mod.rs file to export the new Container struct | ||||
| 3. Create example usage to demonstrate the new API | ||||
| 4. Deprecate the old function-based API (but keep it for backward compatibility) | ||||
| 5. Update documentation to reflect the new API | ||||
|  | ||||
| ## Testing Strategy | ||||
|  | ||||
| 1. Unit tests for the Container struct and its methods | ||||
| 2. Integration tests for the Container API | ||||
| 3. Manual testing with real containers | ||||
| @@ -1 +0,0 @@ | ||||
| EXAMPLE FILE TO TEST | ||||
| @@ -1,8 +0,0 @@ | ||||
| # syntax=docker/dockerfile:1 | ||||
|  | ||||
| FROM node:lts-alpine | ||||
| WORKDIR /app | ||||
| COPY . . | ||||
| RUN yarn install --production | ||||
| CMD ["node", "src/index.js"] | ||||
| EXPOSE 3000 | ||||
| @@ -1,121 +0,0 @@ | ||||
| # Implementation Plan: Rhai Integration for OS Module Functions | ||||
|  | ||||
| ## 1. Project Structure Changes | ||||
|  | ||||
| We'll create a new directory structure for the Rhai integration: | ||||
|  | ||||
| ``` | ||||
| src/ | ||||
| ├── rhai/ | ||||
| │   ├── mod.rs         # Main module file for Rhai integration | ||||
| │   ├── os.rs          # OS module wrappers | ||||
| │   └── error.rs       # Error type conversions | ||||
| ``` | ||||
|  | ||||
| ## 2. Dependencies | ||||
|  | ||||
| Add Rhai as a dependency in Cargo.toml: | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| # Existing dependencies... | ||||
| rhai = { version = "1.12.0", features = ["sync"] } | ||||
| ``` | ||||
|  | ||||
| ## 3. Implementation Steps | ||||
|  | ||||
| ### 3.1. Create Basic Module Structure | ||||
|  | ||||
| 1. Create the `src/rhai/mod.rs` file with: | ||||
|    - Module declarations | ||||
|    - A modular registration system | ||||
|    - Public exports | ||||
|  | ||||
| 2. Create the `src/rhai/error.rs` file with: | ||||
|    - Conversions from our custom error types to Rhai's `EvalAltResult` | ||||
|    - Helper functions for error handling | ||||
|  | ||||
| ### 3.2. Implement OS Module Wrappers | ||||
|  | ||||
| Create the `src/rhai/os.rs` file with: | ||||
|  | ||||
| 1. Import necessary modules and types | ||||
| 2. Create wrapper functions for each function in `src/os/fs.rs` and `src/os/download.rs` | ||||
| 3. Implement a registration function specific to the OS module | ||||
| 4. Expose error types to Rhai | ||||
|  | ||||
| ### 3.3. Update Main Library File | ||||
|  | ||||
| Update `src/lib.rs` to expose the new Rhai module. | ||||
|  | ||||
| ## 4. Detailed Implementation | ||||
|  | ||||
| ### 4.1. Error Handling | ||||
|  | ||||
| For each function that returns a `Result<T, E>` where `E` is one of our custom error types: | ||||
|  | ||||
| 1. Create a wrapper function that converts our error type to Rhai's `EvalAltResult` | ||||
| 2. Register the error types with Rhai to allow for proper error handling in scripts | ||||
|  | ||||
| ### 4.2. Function Wrappers | ||||
|  | ||||
| For each function in the OS module: | ||||
|  | ||||
| 1. Create a wrapper function with the same name | ||||
| 2. Handle any necessary type conversions | ||||
| 3. Convert error types to Rhai's error system | ||||
| 4. Register the function with the Rhai engine | ||||
|  | ||||
| ### 4.3. Registration System | ||||
|  | ||||
| Create a modular registration system: | ||||
|  | ||||
| 1. Implement a general `register` function that takes a Rhai engine | ||||
| 2. Implement module-specific registration functions (e.g., `register_os_module`) | ||||
| 3. Design the system to be extensible for future modules | ||||
|  | ||||
| ## 5. Implementation Flow Diagram | ||||
|  | ||||
| ```mermaid | ||||
| flowchart TD | ||||
|     A[Add Rhai dependency] --> B[Create directory structure] | ||||
|     B --> C[Implement error conversions] | ||||
|     C --> D[Implement OS module wrappers] | ||||
|     D --> E[Create registration system] | ||||
|     E --> F[Update lib.rs] | ||||
|     F --> G[Test the implementation] | ||||
| ``` | ||||
|  | ||||
| ## 6. Function Mapping | ||||
|  | ||||
| Here's a mapping of the OS module functions to their Rhai wrappers: | ||||
|  | ||||
| ### File System Functions (from fs.rs) | ||||
| - `copy` → Wrap with error conversion | ||||
| - `exist` → Direct wrapper (returns bool) | ||||
| - `find_file` → Wrap with error conversion | ||||
| - `find_files` → Wrap with error conversion | ||||
| - `find_dir` → Wrap with error conversion | ||||
| - `find_dirs` → Wrap with error conversion | ||||
| - `delete` → Wrap with error conversion | ||||
| - `mkdir` → Wrap with error conversion | ||||
| - `file_size` → Wrap with error conversion | ||||
| - `rsync` → Wrap with error conversion | ||||
|  | ||||
| ### Download Functions (from download.rs) | ||||
| - `download` → Wrap with error conversion | ||||
| - `download_install` → Wrap with error conversion | ||||
|  | ||||
| ## 7. Error Type Handling | ||||
|  | ||||
| We'll expose our custom error types to Rhai: | ||||
|  | ||||
| 1. Register `FsError` and `DownloadError` as custom types | ||||
| 2. Implement proper error conversion to allow for detailed error handling in Rhai scripts | ||||
| 3. Create helper functions to extract error information | ||||
|  | ||||
| ## 8. Testing Strategy | ||||
|  | ||||
| 1. Create unit tests for each wrapper function | ||||
| 2. Create integration tests with sample Rhai scripts | ||||
| 3. Test error handling scenarios | ||||
| @@ -1,179 +0,0 @@ | ||||
| # Run Builder Implementation Plan | ||||
|  | ||||
| This document outlines the plan for refactoring the `run.rs` module to use the builder pattern. | ||||
|  | ||||
| ## Current Implementation Analysis | ||||
|  | ||||
| The current implementation has several functions for running commands and scripts: | ||||
| - `run_command` and `run_command_silent` for single commands | ||||
| - `run_script` and `run_script_silent` for multiline scripts | ||||
| - `run` and `run_silent` as convenience functions that detect whether the input is a command or script | ||||
|  | ||||
| These functions don't support all the options we want (die, async, log), and they don't follow the builder pattern. | ||||
|  | ||||
| ## Builder Pattern Implementation Plan | ||||
|  | ||||
| ### 1. Create a `RunBuilder` struct | ||||
|  | ||||
| ```rust | ||||
| pub struct RunBuilder<'a> { | ||||
|     cmd: &'a str, | ||||
|     die: bool, | ||||
|     silent: bool, | ||||
|     async_exec: bool, | ||||
|     log: bool, | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 2. Implement Default Values and Builder Methods | ||||
|  | ||||
| ```rust | ||||
| impl<'a> RunBuilder<'a> { | ||||
|     pub fn new(cmd: &'a str) -> Self { | ||||
|         Self { | ||||
|             cmd, | ||||
|             die: true,         // Default: true | ||||
|             silent: false,     // Default: false | ||||
|             async_exec: false, // Default: false | ||||
|             log: false,        // Default: false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn die(mut self, die: bool) -> Self { | ||||
|         self.die = die; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn silent(mut self, silent: bool) -> Self { | ||||
|         self.silent = silent; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn async_exec(mut self, async_exec: bool) -> Self { | ||||
|         self.async_exec = async_exec; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn log(mut self, log: bool) -> Self { | ||||
|         self.log = log; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn execute(self) -> Result<CommandResult, RunError> { | ||||
|         // Implementation will go here | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 3. Implement the `execute` Method | ||||
|  | ||||
| The `execute` method will: | ||||
| 1. Determine if the command is a script or a single command | ||||
| 2. Handle the `async_exec` option by spawning a process without waiting | ||||
| 3. Handle the `log` option by logging command execution if enabled | ||||
| 4. Handle the `die` option by returning a CommandResult instead of an Err when die=false | ||||
| 5. Use the existing internal functions for the actual execution | ||||
|  | ||||
| ### 4. Create a Public Function to Start the Builder | ||||
|  | ||||
| ```rust | ||||
| pub fn run(cmd: &str) -> RunBuilder { | ||||
|     RunBuilder::new(cmd) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 5. Update Existing Functions for Backward Compatibility | ||||
|  | ||||
| Update the existing functions to use the new builder pattern internally for backward compatibility. | ||||
|  | ||||
| ## Structure Diagram | ||||
|  | ||||
| ```mermaid | ||||
| classDiagram | ||||
|     class RunBuilder { | ||||
|         +String cmd | ||||
|         +bool die | ||||
|         +bool silent | ||||
|         +bool async_exec | ||||
|         +bool log | ||||
|         +new(cmd: &str) RunBuilder | ||||
|         +die(bool) RunBuilder | ||||
|         +silent(bool) RunBuilder | ||||
|         +async_exec(bool) RunBuilder | ||||
|         +log(bool) RunBuilder | ||||
|         +execute() Result<CommandResult, RunError> | ||||
|     } | ||||
|      | ||||
|     class CommandResult { | ||||
|         +String stdout | ||||
|         +String stderr | ||||
|         +bool success | ||||
|         +int code | ||||
|     } | ||||
|      | ||||
|     RunBuilder ..> CommandResult : produces | ||||
|      | ||||
|     note for RunBuilder "Builder pattern implementation\nfor command execution" | ||||
| ``` | ||||
|  | ||||
| ## Implementation Details | ||||
|  | ||||
| ### Handling the `async_exec` Option | ||||
|  | ||||
| When `async_exec` is true, we'll spawn the process but not wait for it to complete. We'll return a CommandResult with: | ||||
| - Empty stdout and stderr | ||||
| - success = true (since we don't know the outcome) | ||||
| - code = 0 (since we don't know the exit code) | ||||
|  | ||||
| ### Handling the `log` Option | ||||
|  | ||||
| When `log` is true, we'll log the command execution with a "[LOG]" prefix. For example: | ||||
| ``` | ||||
| [LOG] Executing command: ls -la | ||||
| ``` | ||||
|  | ||||
| ### Handling the `die` Option | ||||
|  | ||||
| When `die` is false and a command fails, instead of returning an Err, we'll return a CommandResult with: | ||||
| - success = false | ||||
| - The appropriate error message in stderr | ||||
| - code = -1 or the actual exit code if available | ||||
|  | ||||
| ## Usage Examples | ||||
|  | ||||
| After implementation, users will be able to use the builder pattern like this: | ||||
|  | ||||
| ```rust | ||||
| // Simple usage with defaults | ||||
| let result = run("ls -la").execute()?; | ||||
|  | ||||
| // With options | ||||
| let result = run("ls -la") | ||||
|     .silent(true) | ||||
|     .die(false) | ||||
|     .execute()?; | ||||
|  | ||||
| // Async execution | ||||
| run("long_running_command") | ||||
|     .async_exec(true) | ||||
|     .execute()?; | ||||
|  | ||||
| // With logging | ||||
| let result = run("important_command") | ||||
|     .log(true) | ||||
|     .execute()?; | ||||
|  | ||||
| // Script execution | ||||
| let result = run("echo 'Hello'\necho 'World'") | ||||
|     .silent(true) | ||||
|     .execute()?; | ||||
| ``` | ||||
|  | ||||
| ## Implementation Steps | ||||
|  | ||||
| 1. Add the `RunBuilder` struct and its methods | ||||
| 2. Implement the `execute` method | ||||
| 3. Create the public `run` function | ||||
| 4. Update the existing functions to use the builder pattern internally | ||||
| 5. Add tests for the new functionality | ||||
| 6. Update documentation | ||||
| @@ -7,11 +7,30 @@ use std::collections::HashMap; | ||||
| use crate::virt::nerdctl::{self, NerdctlError, Image, Container}; | ||||
| use crate::process::CommandResult; | ||||
|  | ||||
| // Helper functions for error conversion | ||||
| // Helper functions for error conversion with improved context | ||||
| fn nerdctl_error_to_rhai_error<T>(result: Result<T, NerdctlError>) -> Result<T, Box<EvalAltResult>> { | ||||
|     result.map_err(|e| { | ||||
|         // Create a more detailed error message based on the error type | ||||
|         let error_message = match &e { | ||||
|             NerdctlError::CommandExecutionFailed(io_err) => { | ||||
|                 format!("Failed to execute nerdctl command: {}. This may indicate nerdctl is not installed or not in PATH.", io_err) | ||||
|             }, | ||||
|             NerdctlError::CommandFailed(msg) => { | ||||
|                 format!("Nerdctl command failed: {}. Check container status and logs for more details.", msg) | ||||
|             }, | ||||
|             NerdctlError::JsonParseError(msg) => { | ||||
|                 format!("Failed to parse nerdctl JSON output: {}. This may indicate an incompatible nerdctl version.", msg) | ||||
|             }, | ||||
|             NerdctlError::ConversionError(msg) => { | ||||
|                 format!("Data conversion error: {}. This may indicate unexpected output format from nerdctl.", msg) | ||||
|             }, | ||||
|             NerdctlError::Other(msg) => { | ||||
|                 format!("Nerdctl error: {}. This is an unexpected error.", msg).. | ||||
|             }, | ||||
|         }; | ||||
|          | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Nerdctl error: {}", e).into(), | ||||
|             error_message.into(), | ||||
|             rhai::Position::NONE | ||||
|         )) | ||||
|     }) | ||||
| @@ -86,9 +105,56 @@ pub fn container_build(container: Container) -> Result<Container, Box<EvalAltRes | ||||
|     nerdctl_error_to_rhai_error(container.build()) | ||||
| } | ||||
|  | ||||
| /// Start the Container | ||||
| /// Start the Container and verify it's running | ||||
| /// | ||||
| /// This function starts the container and verifies that it's actually running. | ||||
| /// It returns detailed error information if the container fails to start or | ||||
| /// if it starts but stops immediately. | ||||
| pub fn container_start(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> { | ||||
|     nerdctl_error_to_rhai_error(container.start()) | ||||
|     // Get container details for better error reporting | ||||
|     let container_name = container.name.clone(); | ||||
|     let container_id = container.container_id.clone().unwrap_or_else(|| "unknown".to_string()); | ||||
|      | ||||
|     // Try to start the container | ||||
|     let start_result = container.start(); | ||||
|      | ||||
|     // Handle the result with improved error context | ||||
|     match start_result { | ||||
|         Ok(result) => { | ||||
|             // Container started successfully | ||||
|             Ok(result) | ||||
|         }, | ||||
|         Err(err) => { | ||||
|             // Add more context to the error | ||||
|             let enhanced_error = match err { | ||||
|                 NerdctlError::CommandFailed(msg) => { | ||||
|                     // Check if this is a "container already running" error, which is not really an error | ||||
|                     if msg.contains("already running") { | ||||
|                         return Ok(CommandResult { | ||||
|                             stdout: format!("Container {} is already running", container_name), | ||||
|                             stderr: "".to_string(), | ||||
|                             success: true, | ||||
|                             code: 0, | ||||
|                         }); | ||||
|                     } | ||||
|                      | ||||
|                     // Try to get more information about why the container might have failed to start | ||||
|                     let mut enhanced_msg = format!("Failed to start container '{}' (ID: {}): {}", | ||||
|                         container_name, container_id, msg); | ||||
|                      | ||||
|                     // Try to check if the image exists | ||||
|                     if let Some(image) = &container.image { | ||||
|                         enhanced_msg.push_str(&format!("\nContainer was using image: {}", image)); | ||||
|                     } | ||||
|                      | ||||
|                     NerdctlError::CommandFailed(enhanced_msg) | ||||
|                 }, | ||||
|                 _ => err | ||||
|             }; | ||||
|              | ||||
|             nerdctl_error_to_rhai_error(Err(enhanced_error)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Stop the Container | ||||
|   | ||||
| @@ -6,14 +6,76 @@ use super::container_types::{Container, ContainerStatus, ResourceUsage}; | ||||
| use serde_json; | ||||
|  | ||||
| impl Container { | ||||
|     /// Start the container | ||||
|     /// Start the container and verify it's running | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// | ||||
|     /// * `Result<CommandResult, NerdctlError>` - Command result or error | ||||
|     /// * `Result<CommandResult, NerdctlError>` - Command result or error with detailed information | ||||
|     pub fn start(&self) -> Result<CommandResult, NerdctlError> { | ||||
|         if let Some(container_id) = &self.container_id { | ||||
|             execute_nerdctl_command(&["start", container_id]) | ||||
|             // First, try to start the container | ||||
|             let start_result = execute_nerdctl_command(&["start", container_id]); | ||||
|              | ||||
|             // If the start command failed, return the error with details | ||||
|             if let Err(err) = &start_result { | ||||
|                 return Err(NerdctlError::CommandFailed( | ||||
|                     format!("Failed to start container {}: {}", container_id, err) | ||||
|                 )); | ||||
|             } | ||||
|              | ||||
|             // Verify the container is actually running | ||||
|             match self.verify_running() { | ||||
|                 Ok(true) => start_result, | ||||
|                 Ok(false) => { | ||||
|                     // Container started but isn't running - try to get more details | ||||
|                     if let Ok(status) = self.status() { | ||||
|                         Err(NerdctlError::CommandFailed( | ||||
|                             format!("Container {} started but is not running. Status: {}, State: {}, Health: {}", | ||||
|                                 container_id, | ||||
|                                 status.status, | ||||
|                                 status.state, | ||||
|                                 status.health_status.unwrap_or_else(|| "N/A".to_string()) | ||||
|                             ) | ||||
|                         )) | ||||
|                     } else { | ||||
|                         Err(NerdctlError::CommandFailed( | ||||
|                             format!("Container {} started but is not running. Unable to get status details.", | ||||
|                                 container_id | ||||
|                             ) | ||||
|                         )) | ||||
|                     } | ||||
|                 }, | ||||
|                 Err(err) => { | ||||
|                     // Failed to verify if container is running | ||||
|                     Err(NerdctlError::CommandFailed( | ||||
|                         format!("Container {} may have started, but verification failed: {}", | ||||
|                             container_id, err | ||||
|                         ) | ||||
|                     )) | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             Err(NerdctlError::Other("No container ID available".to_string())) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Verify if the container is running | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// | ||||
|     /// * `Result<bool, NerdctlError>` - True if running, false if not running, error if verification failed | ||||
|     fn verify_running(&self) -> Result<bool, NerdctlError> { | ||||
|         if let Some(container_id) = &self.container_id { | ||||
|             // Use inspect to check if the container is running | ||||
|             let inspect_result = execute_nerdctl_command(&["inspect", "--format", "{{.State.Running}}", container_id]); | ||||
|              | ||||
|             match inspect_result { | ||||
|                 Ok(result) => { | ||||
|                     let running = result.stdout.trim().to_lowercase() == "true"; | ||||
|                     Ok(running) | ||||
|                 }, | ||||
|                 Err(err) => Err(err) | ||||
|             } | ||||
|         } else { | ||||
|             Err(NerdctlError::Other("No container ID available".to_string())) | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user