This commit is contained in:
despiegk 2025-04-05 06:21:50 +02:00
parent d336153247
commit 245aee12bf
9 changed files with 135 additions and 1306 deletions

View File

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

View File

@ -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

View File

View File

@ -1 +0,0 @@
EXAMPLE FILE TO TEST

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -7,11 +7,30 @@ use std::collections::HashMap;
use crate::virt::nerdctl::{self, NerdctlError, Image, Container}; use crate::virt::nerdctl::{self, NerdctlError, Image, Container};
use crate::process::CommandResult; 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>> { fn nerdctl_error_to_rhai_error<T>(result: Result<T, NerdctlError>) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| { 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( Box::new(EvalAltResult::ErrorRuntime(
format!("Nerdctl error: {}", e).into(), error_message.into(),
rhai::Position::NONE rhai::Position::NONE
)) ))
}) })
@ -86,9 +105,56 @@ pub fn container_build(container: Container) -> Result<Container, Box<EvalAltRes
nerdctl_error_to_rhai_error(container.build()) 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>> { 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 /// Stop the Container

View File

@ -6,14 +6,76 @@ use super::container_types::{Container, ContainerStatus, ResourceUsage};
use serde_json; use serde_json;
impl Container { impl Container {
/// Start the container /// Start the container and verify it's running
/// ///
/// # Returns /// # 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> { pub fn start(&self) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id { 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 { } else {
Err(NerdctlError::Other("No container ID available".to_string())) Err(NerdctlError::Other("No container ID available".to_string()))
} }