Compare commits

..

No commits in common. "7cdd9f555934552a63c0ba76cf72a549aca7661e" and "9f33c940209ce5ac8bc27c7d266f21269230bf15" have entirely different histories.

60 changed files with 2337 additions and 4420 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

115
examples/buildah.rs Normal file
View File

@ -0,0 +1,115 @@
//! 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::{self, 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
println!("\n=== Creating container from fedora:latest ===");
let result = buildah::from("fedora:latest")?;
let container_id = result.stdout.trim();
println!("Created container: {}", container_id);
// Step 2: Run a command in the container
println!("\n=== Installing nginx in container ===");
// Use chroot isolation to avoid BPF issues
let install_result = buildah::bah_run_with_isolation(container_id, "dnf install -y nginx", "chroot")?;
println!("{:#?}", install_result);
println!("Installation output: {}", install_result.stdout);
// Step 3: Copy a file into the container
println!("\n=== Copying configuration file to container ===");
buildah::bah_copy(container_id, "./example.conf", "/etc/example.conf").unwrap();
// 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());
buildah::bah_config(container_id, config_options)?;
buildah::config(container_id, config_options)?;
println!("Container configured");
// Step 5: Commit the container to create a new image
println!("\n=== Committing container to create image ===");
let image_name = "my-nginx:latest";
buildah::image_commit(container_id, image_name, Some("docker"), true, true)?;
println!("Created image: {}", image_name);
// Step 6: List images to verify our new image exists
println!("\n=== Listing images ===");
let images = buildah::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 ===");
buildah::image_remove(image_name).unwrap();
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 = buildah::bah_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 ===");
buildah::image_pull("docker.io/library/alpine:latest", true)?;
println!("Image pulled successfully");
// Tag the image
println!("\n=== Tagging the image ===");
buildah::image_tag("alpine:latest", "my-alpine:v1.0")?;
println!("Image tagged successfully");
// Push an image (this would typically go to a real registry)
// println!("\n=== Pushing an image (example only) ===");
// println!("In a real scenario, you would push to a registry with:");
// println!("buildah::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)");
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().unwrap();
}

View File

@ -1,62 +0,0 @@
// File: /root/code/git.ourworld.tf/herocode/sal/examples/container_example.rs
use std::error::Error;
use sal::virt::nerdctl::Container;
fn main() -> Result<(), Box<dyn Error>> {
// Create a container from an image
println!("Creating container from image...");
let container = Container::from_image("my-nginx", "nginx:latest")?
.with_port("8080:80")
.with_env("NGINX_HOST", "example.com")
.with_volume("/tmp/nginx:/usr/share/nginx/html")
.with_health_check("curl -f http://localhost/ || exit 1")
.with_detach(true)
.build()?;
println!("Container created successfully");
// Execute a command in the container
println!("Executing command in container...");
let result = container.exec("echo 'Hello from container'")?;
println!("Command output: {}", result.stdout);
// Get container status
println!("Getting container status...");
let status = container.status()?;
println!("Container status: {}", status.status);
// Get resource usage
println!("Getting resource usage...");
let resources = container.resources()?;
println!("CPU usage: {}", resources.cpu_usage);
println!("Memory usage: {}", resources.memory_usage);
// Stop and remove the container
println!("Stopping and removing container...");
container.stop()?;
container.remove()?;
println!("Container stopped and removed");
// Get a container by name (if it exists)
println!("\nGetting a container by name...");
match Container::new("existing-container") {
Ok(container) => {
if container.container_id.is_some() {
println!("Found container with ID: {}", container.container_id.unwrap());
// Perform operations on the existing container
let status = container.status()?;
println!("Container status: {}", status.status);
} else {
println!("Container exists but has no ID");
}
},
Err(e) => {
println!("Error getting container: {}", e);
}
}
Ok(())
}

84
examples/nerdctl.rs Normal file
View File

@ -0,0 +1,84 @@
//! Example usage of the nerdctl module
//!
//! This file demonstrates how to use the nerdctl module to perform
//! common container operations like creating containers, running commands,
//! and managing images.
use sal::virt::nerdctl::{self, NerdctlError};
/// Run a complete nerdctl workflow example
pub fn run_nerdctl_example() -> Result<(), NerdctlError> {
println!("Starting nerdctl example workflow...");
// Step 1: Pull an image
println!("\n=== Pulling nginx:latest image ===");
let pull_result = nerdctl::image_pull("nginx:latest")?;
println!("Pull output: {}", pull_result.stdout);
// Step 2: Create a container from the image
println!("\n=== Creating container from nginx:latest ===");
// Use "native" snapshotter to avoid overlay mount issues
let run_result = nerdctl::run("nginx:latest", Some("my-nginx"), true, Some(&["8080:80"]), Some("native"))?;
println!("Container created: {}", run_result.stdout.trim());
let container_id = "my-nginx"; // Using the name we specified
// Step 3: Execute a command in the container
println!("\n=== Installing curl in container ===");
let update_result = nerdctl::exec(container_id, "apt-get update")?;
println!("Update output: {}", update_result.stdout);
let install_result = nerdctl::exec(container_id, "apt-get install -y curl")?;
println!("Installation output: {}", install_result.stdout);
// Step 4: Copy a file into the container (assuming nginx.conf exists)
println!("\n=== Copying configuration file to container ===");
nerdctl::copy("./nginx.conf", format!("{}:/etc/nginx/nginx.conf", container_id).as_str())?;
// Step 5: Commit the container to create a new image
println!("\n=== Committing container to create image ===");
let image_name = "my-custom-nginx:latest";
nerdctl::image_commit(container_id, image_name)?;
println!("Created image: {}", image_name);
// Step 6: Stop and remove the container
println!("\n=== Stopping and removing container ===");
nerdctl::stop(container_id)?;
nerdctl::remove(container_id)?;
println!("Container stopped and removed");
// Step 7: Create a new container from our custom image
println!("\n=== Creating container from custom image ===");
// Use "native" snapshotter to avoid overlay mount issues
nerdctl::run(image_name, Some("nginx-custom"), true, Some(&["8081:80"]), Some("native"))?;
println!("Custom container created");
// Step 8: List images
println!("\n=== Listing images ===");
let images_result = nerdctl::images()?;
println!("Images: \n{}", images_result.stdout);
// Step 9: Clean up (optional in a real workflow)
println!("\n=== Cleaning up ===");
nerdctl::stop("nginx-custom")?;
nerdctl::remove("nginx-custom")?;
nerdctl::image_remove(image_name)?;
println!("\nNerdctl example workflow completed successfully!");
Ok(())
}
/// Main function to run all examples
pub fn run_all_examples() -> Result<(), NerdctlError> {
println!("=== NERDCTL MODULE EXAMPLES ===\n");
run_nerdctl_example()?;
println!("\nNote that these examples require nerdctl to be installed on your system");
println!("and may require root/sudo privileges depending on your setup.");
Ok(())
}
fn main() {
let _ = run_all_examples().unwrap();
}

View File

@ -0,0 +1,74 @@
//! Example of using the Rhai integration with SAL Buildah module
//!
//! This example demonstrates how to use the Rhai scripting language
//! with the System Abstraction Layer (SAL) Buildah module.
use rhai::Engine;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// Create a new Rhai engine
let mut engine = Engine::new();
// Register println function
engine.register_fn("println", |s: &str| println!("{}", s));
// Register SAL functions with the engine
sal::rhai::register(&mut engine)?;
// Run a Rhai script that uses SAL Buildah functions
let script = r#"
// List available images
println("Listing available images:");
let images = buildah_images();
println("Found " + images.len() + " images");
// Create a container from an image (uncomment if you have a valid image)
// let container = buildah_from("alpine:latest");
// println("Created container: " + container.stdout.trim());
// Build an image using options
let build_options = buildah_new_build_options();
build_options.tag = "example-image:latest";
build_options.context_dir = ".";
build_options.file = "example_Dockerfile";
println("Building image with options:");
println(" Tag: " + build_options.tag);
println(" Context: " + build_options.context_dir);
println(" Dockerfile: " + build_options.file);
// Uncomment to actually build the image
// let build_result = buildah_build(build_options);
// println("Build result: " + build_result.success);
// Create a container configuration
let config_options = buildah_new_config_options();
config_options.author = "Rhai Example";
config_options.cmd = "/bin/sh -c 'echo Hello from Buildah'";
println("Container config options:");
println(" Author: " + config_options.author);
println(" Command: " + config_options.cmd);
// Commit options
let commit_options = buildah_new_commit_options();
commit_options.format = "docker";
commit_options.squash = true;
commit_options.rm = true;
println("Commit options:");
println(" Format: " + commit_options.format);
println(" Squash: " + commit_options.squash);
println(" Remove container: " + commit_options.rm);
// Return success
true
"#;
// Evaluate the script
let result = engine.eval::<bool>(script)?;
println!("Script execution successful: {}", result);
Ok(())
}

View File

@ -0,0 +1,54 @@
//! Example of using the Rhai integration with SAL Process module
//!
//! This example demonstrates how to use the Rhai scripting language
//! with the System Abstraction Layer (SAL) Process module.
use rhai::Engine;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// Create a new Rhai engine
let mut engine = Engine::new();
// Register SAL functions with the engine
sal::rhai::register(&mut engine)?;
// Run a Rhai script that uses SAL Process functions
let script = r#"
// Check if a command exists
let ls_exists = which("ls");
println("ls command exists: " + ls_exists);
// Run a simple command
let echo_result = run_command("echo 'Hello from Rhai!'");
println("Echo command output: " + echo_result.stdout);
println("Echo command success: " + echo_result.success);
// Run a command silently
let silent_result = run_silent("ls -la");
println("Silent command success: " + silent_result.success);
// Run a command with custom options using a Map
let options = new_run_options();
options["die"] = false; // Don't return error if command fails
options["silent"] = true; // Suppress output to stdout/stderr
options["async_exec"] = false; // Run synchronously
options["log"] = true; // Log command execution
let custom_result = run("echo 'Custom options'", options);
println("Custom command success: " + custom_result.success);
// List processes
let processes = process_list("");
println("Number of processes: " + processes.len());
// Return success
true
"#;
// Evaluate the script
let result = engine.eval::<bool>(script)?;
println!("Script execution successful: {}", result);
Ok(())
}

View File

@ -0,0 +1,96 @@
// 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}`);
// List available images (only if buildah is installed)
println("Listing available container images:");
// if ! buildah_exists != "" {
// //EXIT
// }
let images = bah_images();
println(`Found ${images.len()} images`);
// Print image details (limited to 3)
let count = 0;
for img in images {
if count >= 3 {
break;
}
println(` - ID: ${img.id}, Name: ${img.name}, Created: ${img.created}`);
count += 1;
}
//Create a container from an image
println("\nCreating a container from alpine image:");
let container = bah_from("alpine:latest");
println(`Container result: success=${container.success}, code=${container.code}`);
println(`Container stdout: "${container.stdout}"`);
println(`Container stderr: "${container.stderr}"`);
let container_id = container.stdout;
println(`Container ID: ${container_id}`);
//Run a command in the container
println("\nRunning a command in the container:");
let run_result = bah_run(container_id, "echo 'Hello from container'");
println(`Command output: ${run_result.stdout}`);
//Add a file to the container
println("\nAdding a file to the container:");
let test_file = "test_file.txt";
run(`echo "Test content" > ${test_file}`);
let add_result = bah_add(container_id, test_file, "/");
println(`Add result: ${add_result.success}`);
//Commit the container to create a new image
println("\nCommitting the container to create a new image:");
let commit_result = bah_commit(container_id, "my-custom-image:latest");
println(`Commit result: ${commit_result.success}`);
//Remove the container
println("\nRemoving the container:");
let remove_result = bah_remove(container_id);
println(`Remove result: ${remove_result.success}`);
//Clean up the test file
delete(test_file);
// Demonstrate build options
println("\nDemonstrating build options:");
let build_options = bah_new_build_options();
build_options.tag = "example-image:latest";
build_options.context_dir = ".";
build_options.file = "example_Dockerfile";
println("Build options configured:");
println(` - Tag: ${build_options.tag}`);
println(` - Context: ${build_options.context_dir}`);
println(` - Dockerfile: ${build_options.file}`);
// Demonstrate commit options
println("\nDemonstrating commit options:");
let commit_options = bah_new_commit_options();
commit_options.format = "docker";
commit_options.squash = true;
commit_options.rm = true;
println("Commit options configured:");
println(` - Format: ${commit_options.format}`);
println(` - Squash: ${commit_options.squash}`);
println(` - Remove container: ${commit_options.rm}`);
// Demonstrate config options
println("\nDemonstrating config options:");
let config_options = bah_new_config_options();
config_options.author = "Rhai Example";
config_options.cmd = "/bin/sh -c 'echo Hello from Buildah'";
println("Config options configured:");
println(` - Author: ${config_options.author}`);
println(` - Command: ${config_options.cmd}`);
"Buildah operations script completed successfully!"

View File

@ -0,0 +1,135 @@
// Test script for Git partial path matching functionality
print("===== Git Path Matching Test =====");
// Test git availability
print("\n=== Checking Git Availability ===");
let git_cmd = which("git");
if git_cmd != false {
print(`Git is available at: ${git_cmd}`);
} else {
print("WARNING: Git is not installed. Tests will be skipped.");
return "Git not available - test skipped";
}
// Helper function for test assertions
fn assert(condition, message) {
if (condition == false) {
print(`FAILED: ${message}`);
throw `Assertion failed: ${message}`;
} else {
print(`PASSED: ${message}`);
}
}
print("\n=== Setting up test repositories ===");
// Get current repos from git_list
let repos = git_list();
print(`Found ${repos.len()} local git repositories for testing`);
if repos.len() == 0 {
print("No repositories found for testing. Creating a test repo...");
// Create a test repo in a temporary directory
let test_dir = "~/tmp_test_repo";
print(`Creating test directory: ${test_dir}`);
// Initialize a git repo with run commands
let result = run_silent(`
mkdir -p ${test_dir}/test1
cd ${test_dir}/test1
git init
echo "Test content" > README.md
git add README.md
git config --global user.email "test@example.com"
git config --global user.name "Test User"
git commit -m "Initial commit"
`);
if result.success {
print("Created test repository successfully");
// Update the repos list
repos = git_list();
print(`Now found ${repos.len()} local git repositories`);
} else {
print("Failed to create test repository");
return "Test setup failed - could not create test repository";
}
}
// Simple function to get just the repo name from the path for easier debugging
fn get_repo_name(path) {
let parts = path.split("/");
return parts[parts.len()-1];
}
print("\n=== Test 1: Testing git_update with exact path ===");
// Using the first repo in the list for testing
let first_repo = repos[0];
print(`Using repository: ${first_repo}`);
print(`Repository name: ${get_repo_name(first_repo)}`);
// Test with exact path
let exact_result = git_update(first_repo);
print(`Result with exact path: ${exact_result}`);
// Check if the result was as expected
// Note: The function might find multiple repos even with exact path
// so we also check for that case
let is_success = exact_result.contains("up to date") ||
exact_result.contains("updated") ||
exact_result.contains("has local changes");
let multiple_match = exact_result.contains("Multiple repositories");
assert(is_success || multiple_match,
"git_update with path should succeed, indicate local changes, or report multiple matches");
print("\n=== Test 2: Testing git_update with partial path ===");
// Extract part of the path (last part of the path)
let repo_name = get_repo_name(first_repo);
print(`Testing partial match with: ${repo_name}`);
let partial_result = git_update(repo_name);
print(`Result with partial path: ${partial_result}`);
// Check if the result was as expected - similar to exact path test
let partial_success = partial_result.contains("up to date") ||
partial_result.contains("updated") ||
partial_result.contains("has local changes");
let partial_multiple = partial_result.contains("Multiple repositories");
assert(partial_success || partial_multiple,
"git_update with partial path should succeed, indicate local changes, or report multiple matches");
print("\n=== Test 3: Testing git_update with non-existent path ===");
let fake_repo = "this_repo_does_not_exist_anywhere";
print(`Testing with non-existent path: ${fake_repo}`);
let nonexist_result = git_update(fake_repo);
print(`Result with non-existent path: ${nonexist_result}`);
// Check that it properly reports an error
assert(nonexist_result.contains("No repositories found"),
"git_update with non-existent path should indicate no matching repos");
print("\n=== Test 4: Testing wildcard matching ===");
// Try to find a common substring in multiple repos
// For this test, we'll use a known path pattern likely to match multiple repos
let common_part = "code";
print(`Testing wildcard match with: ${common_part}*`);
let wildcard_result = git_update(common_part + "*");
print(`Result with wildcard: ${wildcard_result}`);
// For wildcard, we expect at least one match but not an error
// Implementation might return the first match or a success message
let wildcard_success = wildcard_result.contains("up to date") ||
wildcard_result.contains("updated") ||
wildcard_result.contains("has local changes");
// Just check that it didn't report no matches found
assert(!wildcard_result.contains("No repositories found"),
"Wildcard matching should find at least one repository");
print("\n===== All Git path matching tests completed! =====");
"Git path matching functionality works correctly"

124
rhaiexamples/git_test.rhai Normal file
View File

@ -0,0 +1,124 @@
// Git operations test script (primarily focused on validation)
// Note: Many git operations are destructive or require network access,
// so this test primarily validates availability and URL handling.
print("===== Git Operations Test =====");
// Test git availability
print("\n=== Test Git Availability ===");
let git_cmd = which("git");
if git_cmd {
print(`Git is available at: ${git_cmd}`);
} else {
print("WARNING: Git is not installed. Some tests will be skipped.");
}
// Test git URL parsing (testing internal implementation through git operations)
print("\n=== Test Git URL Parsing ===");
// HTTPS URLs
let https_urls = [
"https://github.com/user/repo.git",
"https://github.com/user/repo",
"https://example.com/user/repo.git"
];
print("Testing HTTPS GitHub URLs:");
for url in https_urls {
// For testing purposes, we'll use the URL rather than actually cloning
// Just check if git_clone responds with "already exists" message which indicates
// it recognized and parsed the URL correctly
let result = git_clone(url);
// Check if the result contains a path with expected structure
let contains_path = result.contains("/code/") &&
(result.contains("github.com") || result.contains("example.com"));
print(` URL: ${url}`);
print(` Path structure valid: ${contains_path ? "yes" : "no"}`);
// Extract the expected path components from the parsing
if contains_path {
let parts = result.split("/");
let domain_part = "";
let user_part = "";
let repo_part = "";
let found_code = false;
for i in 0..parts.len() {
if parts[i] == "code" && (i+3) < parts.len() {
domain_part = parts[i+1];
user_part = parts[i+2];
repo_part = parts[i+3];
found_code = true;
break;
}
}
if found_code {
print(` Parsed domain: ${domain_part}`);
print(` Parsed user: ${user_part}`);
print(` Parsed repo: ${repo_part}`);
}
}
}
// SSH URLs
let ssh_urls = [
"git@github.com:user/repo.git",
"git@example.com:organization/repository.git"
];
print("\nTesting SSH Git URLs:");
for url in ssh_urls {
// Similar approach to HTTPS testing
let result = git_clone(url);
let contains_path = result.contains("/code/");
print(` URL: ${url}`);
print(` Path structure valid: ${contains_path ? "yes" : "no"}`);
}
// Test git_list
print("\n=== Test git_list() ===");
let repos = git_list();
print(`Found ${repos.len()} local git repositories`);
if repos.len() > 0 {
print("First 3 repositories (or fewer if less available):");
let max = Math.min(3, repos.len());
for i in 0..max {
print(` ${i+1}. ${repos[i]}`);
}
}
// Test git command access through run
print("\n=== Test Git Command Access ===");
if git_cmd {
let git_version = run("git --version");
print(`Git version info: ${git_version.stdout}`);
// Test git status command (this is safe and doesn't modify anything)
let git_status = run("git status");
print("Git status command:");
print(` Exit code: ${git_status.code}`);
if git_status.success {
print(" Git status executed successfully in current directory");
} else {
print(" Git status failed. This might not be a git repository.");
}
}
// Minimal testing of other git operations (these are mostly destructive)
print("\n=== Git Operations Availability Check ===");
print("The following git operations are available:");
print(" - git_clone: Available (tested above)");
print(" - git_list: Available (tested above)");
print(" - git_update: Available (not tested to avoid modifying repos)");
print(" - git_update_force: Available (not tested to avoid modifying repos)");
print(" - git_update_commit: Available (not tested to avoid modifying repos)");
print(" - git_update_commit_push: Available (not tested to avoid modifying repos)");
print("\nGit Operations Test completed!");
"Git Test Success"

View File

@ -2,69 +2,6 @@
The Buildah module provides functions for working with containers and images using the Buildah tool. Buildah helps you create and manage container images. The Buildah module provides functions for working with containers and images using the Buildah tool. Buildah helps you create and manage container images.
## Builder Pattern
The Buildah module now supports a Builder pattern, which provides a more intuitive and flexible way to work with containers and images.
### Creating a Builder
```rhai
// Create a builder with a name and base image
let builder = bah_new("my-container", "alpine:latest");
// Access builder properties
let container_id = builder.container_id;
let name = builder.name;
let image = builder.image;
```
### Builder Methods
The Builder object provides the following methods:
- `run(command)`: Run a command in the container
- `run_with_isolation(command, isolation)`: Run a command with specified isolation
- `copy(source, dest)`: Copy files into the container
- `add(source, dest)`: Add files into the container
- `commit(image_name)`: Commit the container to an image
- `remove()`: Remove the container
- `reset()`: Remove the container and clear the container_id
- `config(options)`: Configure container metadata
- `images()`: List images in local storage
- `image_remove(image)`: Remove an image
- `image_pull(image, tls_verify)`: Pull an image from a registry
- `image_push(image, destination, tls_verify)`: Push an image to a registry
- `image_tag(image, new_name)`: Add a tag to an image
- `build(tag, context_dir, file, isolation)`: Build an image from a Dockerfile
### Example
```rhai
// Create a builder
let builder = bah_new("my-container", "alpine:latest");
// Reset the builder to remove any existing container
builder.reset();
// Create a new container
builder = bah_new("my-container", "alpine:latest");
// Run a command
let result = builder.run("echo 'Hello from container'");
println(`Command output: ${result.stdout}`);
// Add a file
file_write("test_file.txt", "Test content");
builder.add("test_file.txt", "/");
// Commit to an image
builder.commit("my-custom-image:latest");
// Clean up
builder.remove();
delete("test_file.txt");
```
## Image Information ## Image Information
### Image Properties ### Image Properties
@ -77,42 +14,332 @@ When working with images, you can access the following information:
- `size`: The size of the image - `size`: The size of the image
- `created`: When the image was created - `created`: When the image was created
## Builder Methods ## Container Functions
### `bah_new(name, image)` ### `bah_from(image)`
Creates a new Builder object for working with a container. Creates a container from an image.
**Parameters:** **Parameters:**
- `name` (string): The name to give the container
- `image` (string): The name or ID of the image to create the container from - `image` (string): The name or ID of the image to create the container from
**Returns:** A Builder object if successful. **Returns:** The ID of the newly created container if successful.
**Example:**
```rhai
// Create a container from an image
let result = bah_from("alpine:latest");
let container_id = result.stdout;
print(`Created container: ${container_id}`);
```
### `bah_run(container, command)`
Runs a command in a container.
**Parameters:**
- `container` (string): The container ID or name
- `command` (string): The command to run
**Returns:** The output of the command if successful.
**Example:** **Example:**
```rhai ```rhai
// Create a new Builder // Run a command in a container
let builder = bah_new("my-container", "alpine:latest"); let result = bah_run("my-container", "echo 'Hello from container'");
print(result.stdout);
``` ```
**Notes:** ### `bah_run_with_isolation(container, command, isolation)`
- If a container with the given name already exists, it will be reused instead of creating a new one
- The Builder object provides methods for working with the container
### `reset()` Runs a command in a container with specified isolation.
Resets a Builder by removing the container and clearing the container_id. This allows you to start fresh with the same Builder object. **Parameters:**
- `container` (string): The container ID or name
- `command` (string): The command to run
- `isolation` (string): The isolation type (e.g., "chroot", "rootless", "oci")
**Returns:** Nothing. **Returns:** The output of the command if successful.
**Example:** **Example:**
```rhai ```rhai
// Create a Builder // Run a command with specific isolation
let builder = bah_new("my-container", "alpine:latest"); let result = bah_run_with_isolation("my-container", "ls -la", "chroot");
print(result.stdout);
// Reset the Builder to remove the container ```
builder.reset();
### `bah_copy(container, source, dest)`
// Create a new container with the same name
builder = bah_new("my-container", "alpine:latest"); Copies files into a container.
**Parameters:**
- `container` (string): The container ID or name
- `source` (string): The source path on the host
- `dest` (string): The destination path in the container
**Returns:** A success message if the copy operation worked.
**Example:**
```rhai
// Copy a file into a container
bah_copy("my-container", "./app.js", "/app/app.js");
```
### `bah_add(container, source, dest)`
Adds files into a container. Similar to `bah_copy` but can also handle remote URLs.
**Parameters:**
- `container` (string): The container ID or name
- `source` (string): The source path on the host or a URL
- `dest` (string): The destination path in the container
**Returns:** A success message if the add operation worked.
**Example:**
```rhai
// Add a file from a URL into a container
bah_add("my-container", "https://example.com/file.tar.gz", "/app/");
```
### `bah_commit(container, image_name)`
Commits a container to an image.
**Parameters:**
- `container` (string): The container ID or name
- `image_name` (string): The name to give the new image
**Returns:** A success message if the commit operation worked.
**Example:**
```rhai
// Commit a container to an image
bah_commit("my-container", "my-image:latest");
```
### `bah_remove(container)`
Removes a container.
**Parameters:**
- `container` (string): The container ID or name
**Returns:** A success message if the container was removed.
**Example:**
```rhai
// Remove a container
bah_remove("my-container");
```
### `bah_list()`
Lists containers.
**Returns:** A list of containers if successful.
**Example:**
```rhai
// List containers
let result = bah_list();
print(result.stdout);
```
### `bah_new_build_options()`
Creates a new map with default build options.
**Returns:** A map with the following default options:
- `tag` (unit/null): The tag for the image (default: null)
- `context_dir` (string): The build context directory (default: ".")
- `file` (string): The Dockerfile path (default: "Dockerfile")
- `isolation` (unit/null): The isolation type (default: null)
**Example:**
```rhai
// Create build options
let options = bah_new_build_options();
```
### `bah_build(options)`
Builds an image with options specified in a map.
**Parameters:**
- `options` (map): A map of options created with `bah_new_build_options()`
**Returns:** A success message if the build operation worked.
**Example:**
```rhai
// Create and customize build options
let options = bah_new_build_options();
options.tag = "my-image:latest";
options.context_dir = "./app";
options.file = "Dockerfile.prod";
options.isolation = "chroot";
// Build an image with options
let result = bah_build(options);
```
## Image Functions
### `bah_images()`
Lists images in local storage.
**Returns:** A list of images if successful.
**Example:**
```rhai
// List images
let images = bah_images();
// Display image information
for image in images {
print(`ID: ${image.id}, Name: ${image.name}, Size: ${image.size}, Created: ${image.created}`);
}
```
### `bah_image_remove(image)`
Removes one or more images.
**Parameters:**
- `image` (string): The image ID or name
**Returns:** A success message if the image was removed.
**Example:**
```rhai
// Remove an image
bah_image_remove("my-image:latest");
```
### `bah_image_push(image, destination, tls_verify)`
Pushes an image to a registry.
**Parameters:**
- `image` (string): The image ID or name
- `destination` (string): The destination registry/repository
- `tls_verify` (boolean): Whether to verify TLS certificates
**Returns:** A success message if the image was pushed.
**Example:**
```rhai
// Push an image to a registry
bah_image_push("my-image:latest", "registry.example.com/my-repo/my-image:latest", true);
```
### `bah_image_tag(image, new_name)`
Adds an additional name to a local image.
**Parameters:**
- `image` (string): The image ID or name
- `new_name` (string): The new name to add
**Returns:** A success message if the image was tagged.
**Example:**
```rhai
// Tag an image with a new name
bah_image_tag("my-image:latest", "my-image:v1.0");
```
### `bah_image_pull(image, tls_verify)`
Pulls an image from a registry.
**Parameters:**
- `image` (string): The image to pull
- `tls_verify` (boolean): Whether to verify TLS certificates
**Returns:** A success message if the image was pulled.
**Example:**
```rhai
// Pull an image from a registry
bah_image_pull("alpine:latest", true);
```
### `bah_new_commit_options()`
Creates a new map with default commit options.
**Returns:** A map with the following default options:
- `format` (unit/null): The format of the image (default: null)
- `squash` (boolean): Whether to squash layers (default: false)
- `rm` (boolean): Whether to remove the container after commit (default: false)
**Example:**
```rhai
// Create commit options
let options = bah_new_commit_options();
```
### `bah_image_commit(container, image_name, options)`
Commits a container to an image with options specified in a map.
**Parameters:**
- `container` (string): The container ID or name
- `image_name` (string): The name to give the new image
- `options` (map): A map of options created with `bah_new_commit_options()`
**Returns:** A success message if the image was created.
**Example:**
```rhai
// Create and customize commit options
let options = bah_new_commit_options();
options.format = "docker";
options.squash = true;
options.rm = true;
// Commit a container to an image with options
let result = bah_image_commit("my-container", "my-image:latest", options);
```
### `bah_new_config_options()`
Creates a new map for config options.
**Returns:** An empty map to be filled with configuration options.
**Example:**
```rhai
// Create config options
let options = bah_new_config_options();
```
### `bah_config(container, options)`
Configures a container with options specified in a map.
**Parameters:**
- `container` (string): The container ID or name
- `options` (map): A map of options created with `bah_new_config_options()`
**Returns:** A success message if the container was configured.
**Example:**
```rhai
// Create and customize config options
let options = bah_new_config_options();
options.author = "John Doe";
options.cmd = "echo Hello";
options.entrypoint = "/bin/sh -c";
options.workingdir = "/app";
options.env = "NODE_ENV=production";
options.label = "version=1.0";
// Configure a container with options
let result = bah_config("my-container", options);
``` ```

View File

@ -2,203 +2,112 @@
This module provides Rhai wrappers for the Git functionality in SAL. This module provides Rhai wrappers for the Git functionality in SAL.
> **Note:** The constructor for GitTree has been renamed from `new()` to `gittree_new()` to avoid confusion with other constructors. This makes the interface more explicit and less likely to cause naming conflicts. ## Basic Git Operations
## Object-Oriented Design ### Clone a Repository
The Git module follows an object-oriented design with two main classes:
1. **GitTree** - Represents a collection of git repositories under a base path
- Created with `gittree_new(base_path)`
- Methods for listing, finding, and getting repositories
2. **GitRepo** - Represents a single git repository
- Obtained from GitTree's `get()` method
- Methods for common git operations: pull, reset, push, commit
This design allows for a more intuitive and flexible interface, with method chaining for complex operations.
## Creating a GitTree
The GitTree object is the main entry point for git operations. It represents a collection of git repositories under a base path.
```rhai ```rhai
// Create a new GitTree with a base path // Clone a repository to a standardized location in the user's home directory
let git_tree = gittree_new("/root/code"); let repo_path = git_clone("https://github.com/username/repo.git");
print(`Created GitTree with base path: /home/user/code`); print(`Repository cloned to: ${repo_path}`);
``` ```
## Finding Repositories ### List Repositories
### List All Repositories
```rhai ```rhai
// List all git repositories under the base path // List all git repositories in the user's ~/code directory
let repos = git_tree.list(); let repos = git_list();
print(`Found ${repos.len()} repositories`); print("Found repositories:");
// Print the repositories
for repo in repos { for repo in repos {
print(` - ${repo}`); print(` - ${repo}`);
} }
``` ```
### Find Repositories Matching a Pattern ### Find Repositories
```rhai ```rhai
// Find repositories matching a pattern // Find repositories matching a pattern
// Use a wildcard (*) suffix to find multiple matches // Use a wildcard (*) suffix to find multiple matches
let matching_repos = git_tree.find("my-project*"); let matching_repos = find_matching_repos("my-project*");
print("Matching repositories:"); print("Matching repositories:");
for repo in matching_repos { for repo in matching_repos {
print(` - ${repo}`); print(` - ${repo}`);
} }
// Find a specific repository (must match exactly one) // Find a specific repository (must match exactly one)
let specific_repo = git_tree.find("unique-project")[0]; let specific_repo = find_matching_repos("unique-project")[0];
print(`Found specific repository: ${specific_repo}`); print(`Found specific repository: ${specific_repo}`);
``` ```
## Working with Repositories
### Get Repository Objects
```rhai
// Get GitRepo objects for repositories matching a pattern
let repos = git_tree.get("my-project*");
print(`Found ${repos.len()} repositories`);
// Get a specific repository
let repo = git_tree.get("unique-project")[0];
print(`Working with repository: ${repo.path()}`);
```
### Clone a Repository
```rhai
// Clone a repository by URL
// This will clone the repository to the base path of the GitTree
let repos = git_tree.get("https://github.com/username/repo.git");
let repo = repos[0];
print(`Repository cloned to: ${repo.path()}`);
```
### Check for Changes ### Check for Changes
```rhai ```rhai
// Check if a repository has uncommitted changes // Check if a repository has uncommitted changes
let repo = git_tree.get("my-project")[0]; let repo_path = "/path/to/repo";
if repo.has_changes() { if git_has_changes(repo_path) {
print("Repository has uncommitted changes"); print("Repository has uncommitted changes");
} else { } else {
print("Repository is clean"); print("Repository is clean");
} }
``` ```
## Repository Operations ## Repository Updates
### Pull Changes ### Update a Repository
```rhai ```rhai
// Pull the latest changes from the remote // Update a repository by pulling the latest changes
// This will fail if there are uncommitted changes // This will fail if there are uncommitted changes
let repo = git_tree.get("my-project")[0]; let result = git_update("my-project");
let result = repo.pull(); print(result);
print("Repository updated successfully");
``` ```
### Reset Local Changes ### Force Update a Repository
```rhai ```rhai
// Reset any local changes in the repository // Force update a repository by discarding local changes and pulling the latest changes
let repo = git_tree.get("my-project")[0]; let result = git_update_force("my-project");
let result = repo.reset(); print(result);
print("Repository reset successfully");
``` ```
### Commit Changes ### Commit and Update
```rhai ```rhai
// Commit changes in the repository // Commit changes in a repository and then update it by pulling the latest changes
let repo = git_tree.get("my-project")[0]; let result = git_update_commit("my-project", "Fix bug in login form");
let result = repo.commit("Fix bug in login form"); print(result);
print("Changes committed successfully");
``` ```
### Push Changes ### Commit and Push
```rhai ```rhai
// Push changes to the remote // Commit changes in a repository and push them to the remote
let repo = git_tree.get("my-project")[0]; let result = git_update_commit_push("my-project", "Add new feature");
let result = repo.push(); print(result);
print("Changes pushed successfully");
``` ```
## Method Chaining
The GitRepo methods can be chained together for more complex operations:
```rhai
// Commit changes and push them to the remote
let repo = git_tree.get("my-project")[0];
let result = repo.commit("Add new feature").push();
print("Changes committed and pushed successfully");
// Reset local changes, pull the latest changes, and commit new changes
let repo = git_tree.get("my-project")[0];
let result = repo.reset().pull().commit("Update dependencies");
print("Repository updated successfully");
```
## Complete Example ## Complete Example
```rhai ```rhai
// Create a new GitTree
let home_dir = env("HOME");
let git_tree = gittree_new(`${home_dir}/code`);
// Clone a repository // Clone a repository
let repos = git_tree.get("https://github.com/username/example-repo.git"); let repo_url = "https://github.com/username/example-repo.git";
let repo = repos[0]; let repo_path = git_clone(repo_url);
print(`Cloned repository to: ${repo.path()}`); print(`Cloned repository to: ${repo_path}`);
// Make some changes (using OS module functions) // Make some changes (using OS module functions)
let file_path = `${repo.path()}/README.md`; let file_path = `${repo_path}/README.md`;
let content = "# Example Repository\n\nThis is an example repository."; let content = "# Example Repository\n\nThis is an example repository.";
write_file(file_path, content); write_file(file_path, content);
// Commit and push the changes // Commit and push the changes
let result = repo.commit("Update README.md").push(); let commit_message = "Update README.md";
print("Changes committed and pushed successfully"); let result = git_update_commit_push(repo_path, commit_message);
print(result);
// List all repositories // List all repositories
let all_repos = git_tree.list(); let repos = git_list();
print("All repositories:"); print("All repositories:");
for repo_path in all_repos { for repo in repos {
print(` - ${repo_path}`); print(` - ${repo}`);
} }
```
## Error Handling
All methods in the Git module return a Result type, which means they can either succeed or fail with an error. If an error occurs, it will be propagated to the Rhai script as a runtime error.
For example, if you try to clone a repository that doesn't exist:
```rhai
// Try to clone a non-existent repository
try {
let git_tree = gittree_new("/root/code");
let repos = git_tree.get("https://github.com/nonexistent/repo.git");
print("This will not be executed if the repository doesn't exist");
} catch(err) {
print(`Error: ${err}`); // Will print the error message from git
}
```
Common errors include:
- Invalid URL
- Repository not found
- Authentication failure
- Network issues
- Local changes exist when trying to pull

66
src/examples/git_test.rs Normal file
View File

@ -0,0 +1,66 @@
//! Example of using the Git module with Rhai
//!
//! This example demonstrates how to use the Git module functions
//! through the Rhai scripting language.
use sal::rhai::{self, Engine};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new Rhai engine
let mut engine = Engine::new();
// Register SAL functions with the engine
rhai::register(&mut engine)?;
// Run a Rhai script that uses Git functions
let script = r#"
// Print a header
print("=== Testing Git Module Functions ===\n");
// Test git_list function
print("Listing git repositories...");
let repos = git_list();
print(`Found ${repos.len()} repositories`);
// Print the first few repositories
if repos.len() > 0 {
print("First few repositories:");
let count = if repos.len() > 3 { 3 } else { repos.len() };
for i in range(0, count) {
print(` - ${repos[i]}`);
}
}
// Test find_matching_repos function
if repos.len() > 0 {
print("\nTesting repository search...");
// Extract a part of the first repo name to search for
let repo_path = repos[0];
let parts = repo_path.split("/");
let repo_name = parts[parts.len() - 1];
print(`Searching for repositories containing "${repo_name}"`);
let matching = find_matching_repos(repo_name);
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
// Check if a repository has changes
print("\nChecking for changes in repository...");
let has_changes = git_has_changes(repo_path);
print(`Repository ${repo_path} has changes: ${has_changes}`);
}
print("\n=== Git Module Test Complete ===");
"#;
// Evaluate the script
match engine.eval::<()>(script) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Script execution error: {}", e),
}
Ok(())
}

View File

@ -0,0 +1,66 @@
//! Example of using the Git module with Rhai
//!
//! This example demonstrates how to use the Git module functions
//! through the Rhai scripting language.
use sal::rhai::{self, Engine};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new Rhai engine
let mut engine = Engine::new();
// Register SAL functions with the engine
rhai::register(&mut engine)?;
// Run a Rhai script that uses Git functions
let script = r#"
// Print a header
print("=== Testing Git Module Functions ===\n");
// Test git_list function
print("Listing git repositories...");
let repos = git_list();
print(`Found ${repos.len()} repositories`);
// Print the first few repositories
if repos.len() > 0 {
print("First few repositories:");
let count = if repos.len() > 3 { 3 } else { repos.len() };
for i in range(0, count) {
print(` - ${repos[i]}`);
}
}
// Test find_matching_repos function
if repos.len() > 0 {
print("\nTesting repository search...");
// Extract a part of the first repo name to search for
let repo_path = repos[0];
let parts = repo_path.split("/");
let repo_name = parts[parts.len() - 1];
print(`Searching for repositories containing "${repo_name}"`);
let matching = find_matching_repos(repo_name);
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
// Check if a repository has changes
print("\nChecking for changes in repository...");
let has_changes = git_has_changes(repo_path);
print(`Repository ${repo_path} has changes: ${has_changes}`);
}
print("\n=== Git Module Test Complete ===");
"#;
// Evaluate the script
match engine.eval::<()>(script) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Script execution error: {}", e),
}
Ok(())
}

View File

@ -11,7 +11,6 @@ use std::error::Error;
pub enum GitError { pub enum GitError {
GitNotInstalled(std::io::Error), GitNotInstalled(std::io::Error),
InvalidUrl(String), InvalidUrl(String),
InvalidBasePath(String),
HomeDirectoryNotFound(std::env::VarError), HomeDirectoryNotFound(std::env::VarError),
FileSystemError(std::io::Error), FileSystemError(std::io::Error),
GitCommandFailed(String), GitCommandFailed(String),
@ -29,7 +28,6 @@ impl fmt::Display for GitError {
match self { match self {
GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e), GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e),
GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url), GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url),
GitError::InvalidBasePath(path) => write!(f, "Invalid base path: {}", path),
GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e), GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e),
GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e), GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e),
GitError::GitCommandFailed(e) => write!(f, "{}", e), GitError::GitCommandFailed(e) => write!(f, "{}", e),
@ -57,21 +55,98 @@ impl Error for GitError {
} }
} }
/// Parses a git URL to extract the server, account, and repository name. // Git utility functions
///
/// # Arguments /**
/// * Clones a git repository to a standardized location in the user's home directory.
/// * `url` - The URL of the git repository to parse. Can be in HTTPS format *
/// (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git). * # Arguments
/// *
/// # Returns * * `url` - The URL of the git repository to clone. Can be in HTTPS format
/// * (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
/// A tuple containing: *
/// * `server` - The server name (e.g., "github.com") * # Returns
/// * `account` - The account or organization name (e.g., "username") *
/// * `repo` - The repository name (e.g., "repo") * * `Ok(String)` - The path where the repository was cloned, formatted as
/// * ~/code/server/account/repo (e.g., ~/code/github.com/username/repo).
/// If the URL cannot be parsed, all three values will be empty strings. * * `Err(GitError)` - An error if the clone operation failed.
*
* # Examples
*
* ```
* let repo_path = git_clone("https://github.com/username/repo.git")?;
* println!("Repository cloned to: {}", repo_path);
* ```
*/
pub fn git_clone(url: &str) -> Result<String, GitError> {
// Check if git is installed
let _git_check = Command::new("git")
.arg("--version")
.output()
.map_err(GitError::GitNotInstalled)?;
// Parse the URL to determine the clone path
let (server, account, repo) = parse_git_url(url);
if server.is_empty() || account.is_empty() || repo.is_empty() {
return Err(GitError::InvalidUrl(url.to_string()));
}
// Create the target directory
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
let clone_path = format!("{}/code/{}/{}/{}", home_dir, server, account, repo);
let clone_dir = Path::new(&clone_path);
// Check if repo already exists
if clone_dir.exists() {
return Ok(format!("Repository already exists at {}", clone_path));
}
// Create parent directory
if let Some(parent) = clone_dir.parent() {
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?;
}
// Clone the repository
let output = Command::new("git")
.args(&["clone", "--depth", "1", url, &clone_path])
.output()
.map_err(GitError::CommandExecutionError)?;
if output.status.success() {
Ok(clone_path)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
}
}
/**
* Parses a git URL to extract the server, account, and repository name.
*
* # Arguments
*
* * `url` - The URL of the git repository to parse. Can be in HTTPS format
* (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
*
* # Returns
*
* A tuple containing:
* * `server` - The server name (e.g., "github.com")
* * `account` - The account or organization name (e.g., "username")
* * `repo` - The repository name (e.g., "repo")
*
* If the URL cannot be parsed, all three values will be empty strings.
*
* # Examples
*
* ```
* let (server, account, repo) = parse_git_url("https://github.com/username/repo.git");
* assert_eq!(server, "github.com");
* assert_eq!(account, "username");
* assert_eq!(repo, "repo");
* ```
*/
pub fn parse_git_url(url: &str) -> (String, String, String) { pub fn parse_git_url(url: &str) -> (String, String, String) {
// HTTP(S) URL format: https://github.com/username/repo.git // HTTP(S) URL format: https://github.com/username/repo.git
let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap(); let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
@ -96,394 +171,427 @@ pub fn parse_git_url(url: &str) -> (String, String, String) {
(String::new(), String::new(), String::new()) (String::new(), String::new(), String::new())
} }
/// Checks if git is installed on the system. /**
/// * Lists all git repositories found in the user's ~/code directory.
/// # Returns *
/// * This function searches for directories containing a .git subdirectory,
/// * `Ok(())` - If git is installed * which indicates a git repository.
/// * `Err(GitError)` - If git is not installed *
fn check_git_installed() -> Result<(), GitError> { * # Returns
Command::new("git") *
.arg("--version") * * `Ok(Vec<String>)` - A vector of paths to git repositories
.output() * * `Err(GitError)` - An error if the operation failed
.map_err(GitError::GitNotInstalled)?; *
Ok(()) * # Examples
} *
* ```
* let repos = git_list()?;
* for repo in repos {
* println!("Found repository: {}", repo);
* }
* ```
*/
pub fn git_list() -> Result<Vec<String>, GitError> {
// Get home directory
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
/// Represents a collection of git repositories under a base path. let code_dir = format!("{}/code", home_dir);
#[derive(Clone)] let code_path = Path::new(&code_dir);
pub struct GitTree {
base_path: String,
}
impl GitTree { if !code_path.exists() || !code_path.is_dir() {
/// Creates a new GitTree with the specified base path. return Ok(Vec::new());
///
/// # Arguments
///
/// * `base_path` - The base path where all git repositories are located
///
/// # Returns
///
/// * `Ok(GitTree)` - A new GitTree instance
/// * `Err(GitError)` - If the base path is invalid or cannot be created
pub fn new(base_path: &str) -> Result<Self, GitError> {
// Check if git is installed
check_git_installed()?;
// Validate the base path
let path = Path::new(base_path);
if !path.exists() {
fs::create_dir_all(path).map_err(|e| {
GitError::FileSystemError(e)
})?;
} else if !path.is_dir() {
return Err(GitError::InvalidBasePath(base_path.to_string()));
}
Ok(GitTree {
base_path: base_path.to_string(),
})
} }
/// Lists all git repositories under the base path. let mut repos = Vec::new();
///
/// # Returns
///
/// * `Ok(Vec<String>)` - A vector of paths to git repositories
/// * `Err(GitError)` - If the operation failed
pub fn list(&self) -> Result<Vec<String>, GitError> {
let base_path = Path::new(&self.base_path);
if !base_path.exists() || !base_path.is_dir() { // Find all directories with .git subdirectories
return Ok(Vec::new()); let output = Command::new("find")
} .args(&[&code_dir, "-type", "d", "-name", ".git"])
.output()
.map_err(GitError::CommandExecutionError)?;
let mut repos = Vec::new(); if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
// Find all directories with .git subdirectories for line in stdout.lines() {
let output = Command::new("find") // Get the parent directory of .git which is the repo root
.args(&[&self.base_path, "-type", "d", "-name", ".git"]) if let Some(parent) = Path::new(line).parent() {
.output() if let Some(path_str) = parent.to_str() {
.map_err(GitError::CommandExecutionError)?; repos.push(path_str.to_string());
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
// Get the parent directory of .git which is the repo root
if let Some(parent) = Path::new(line).parent() {
if let Some(path_str) = parent.to_str() {
repos.push(path_str.to_string());
}
} }
} }
} else {
let error = String::from_utf8_lossy(&output.stderr);
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
} }
} else {
Ok(repos) let error = String::from_utf8_lossy(&output.stderr);
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
} }
/// Finds repositories matching a pattern or partial path. Ok(repos)
/// }
/// # Arguments
///
/// * `pattern` - The pattern to match against repository paths
/// - If the pattern ends with '*', all matching repositories are returned
/// - Otherwise, exactly one matching repository must be found
///
/// # Returns
///
/// * `Ok(Vec<String>)` - A vector of paths to matching repositories
/// * `Err(GitError)` - If no matching repositories are found,
/// or if multiple repositories match a non-wildcard pattern
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> {
// Get all repos
let repos = self.list()?;
if repos.is_empty() { /**
return Err(GitError::NoRepositoriesFound); * Checks if a git repository has uncommitted changes.
} *
* # Arguments
*
* * `repo_path` - The path to the git repository
*
* # Returns
*
* * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
* * `Err(GitError)` - An error if the operation failed
*
* # Examples
*
* ```
* if has_git_changes("/path/to/repo")? {
* println!("Repository has uncommitted changes");
* } else {
* println!("Repository is clean");
* }
* ```
*/
pub fn has_git_changes(repo_path: &str) -> Result<bool, GitError> {
let output = Command::new("git")
.args(&["-C", repo_path, "status", "--porcelain"])
.output()
.map_err(GitError::CommandExecutionError)?;
// Check if pattern ends with wildcard Ok(!output.stdout.is_empty())
if pattern.ends_with('*') { }
let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
let matching: Vec<String> = repos.iter()
.filter(|repo| repo.contains(search_pattern))
.cloned()
.collect();
if matching.is_empty() { /**
return Err(GitError::RepositoryNotFound(pattern.to_string())); * Finds repositories matching a pattern or partial path.
} *
* # Arguments
*
* * `pattern` - The pattern to match against repository paths
* - If the pattern ends with '*', all matching repositories are returned
* - Otherwise, exactly one matching repository must be found
*
* # Returns
*
* * `Ok(Vec<String>)` - A vector of paths to matching repositories
* * `Err(GitError)` - An error if no matching repositories are found,
* or if multiple repositories match a non-wildcard pattern
*
* # Examples
*
* ```
* // Find all repositories containing "project"
* let repos = find_matching_repos("project*")?;
*
* // Find exactly one repository containing "unique-project"
* let repo = find_matching_repos("unique-project")?[0];
* ```
*/
pub fn find_matching_repos(pattern: &str) -> Result<Vec<String>, GitError> {
// Get all repos
let repos = git_list()?;
Ok(matching) if repos.is_empty() {
} else { return Err(GitError::NoRepositoriesFound);
// No wildcard, need to find exactly one match
let matching: Vec<String> = repos.iter()
.filter(|repo| repo.contains(pattern))
.cloned()
.collect();
match matching.len() {
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
1 => Ok(matching),
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
}
}
} }
/// Gets one or more GitRepo objects based on a path pattern or URL. // Check if pattern ends with wildcard
/// if pattern.ends_with('*') {
/// # Arguments let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
/// let matching: Vec<String> = repos.iter()
/// * `path_or_url` - The path pattern to match against repository paths or a git URL .filter(|repo| repo.contains(search_pattern))
/// - If it's a URL, the repository will be cloned if it doesn't exist .cloned()
/// - If it's a path pattern, it will find matching repositories .collect();
///
/// # Returns
///
/// * `Ok(Vec<GitRepo>)` - A vector of GitRepo objects
/// * `Err(GitError)` - If no matching repositories are found or the clone operation failed
pub fn get(&self, path_or_url: &str) -> Result<Vec<GitRepo>, GitError> {
// Check if it's a URL
if path_or_url.starts_with("http") || path_or_url.starts_with("git@") {
// Parse the URL
let (server, account, repo) = parse_git_url(path_or_url);
if server.is_empty() || account.is_empty() || repo.is_empty() {
return Err(GitError::InvalidUrl(path_or_url.to_string()));
}
// Create the target directory if matching.is_empty() {
let clone_path = format!("{}/{}/{}/{}", self.base_path, server, account, repo); return Err(GitError::RepositoryNotFound(pattern.to_string()));
let clone_dir = Path::new(&clone_path); }
// Check if repo already exists Ok(matching)
if clone_dir.exists() { } else {
return Ok(vec![GitRepo::new(clone_path)]); // No wildcard, need to find exactly one match
} let matching: Vec<String> = repos.iter()
.filter(|repo| repo.contains(pattern))
.cloned()
.collect();
// Create parent directory match matching.len() {
if let Some(parent) = clone_dir.parent() { 0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?; 1 => Ok(matching),
} _ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
// Clone the repository
let output = Command::new("git")
.args(&["clone", "--depth", "1", path_or_url, &clone_path])
.output()
.map_err(GitError::CommandExecutionError)?;
if output.status.success() {
Ok(vec![GitRepo::new(clone_path)])
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
}
} else {
// It's a path pattern, find matching repositories
let repo_paths = self.find(path_or_url)?;
// Convert paths to GitRepo objects
let repos: Vec<GitRepo> = repo_paths.into_iter()
.map(GitRepo::new)
.collect();
Ok(repos)
} }
} }
} }
/// Represents a git repository. /**
pub struct GitRepo { * Updates a git repository by pulling the latest changes.
path: String, *
} * This function will fail if there are uncommitted changes in the repository.
*
* # Arguments
*
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
*
* # Returns
*
* * `Ok(String)` - A success message indicating the repository was updated
* * `Err(GitError)` - An error if the update failed
*
* # Examples
*
* ```
* let result = git_update("my-project")?;
* println!("{}", result); // "Successfully updated repository at /home/user/code/github.com/user/my-project"
* ```
*/
pub fn git_update(repo_path: &str) -> Result<String, GitError> {
// If repo_path may be a partial path, find the matching repository
let repos = find_matching_repos(repo_path)?;
impl GitRepo { // Should only be one repository at this point
/// Creates a new GitRepo with the specified path. let actual_path = &repos[0];
///
/// # Arguments // Check if repository exists and is a git repository
/// let git_dir = Path::new(actual_path).join(".git");
/// * `path` - The path to the git repository if !git_dir.exists() || !git_dir.is_dir() {
pub fn new(path: String) -> Self { return Err(GitError::NotAGitRepository(actual_path.clone()));
GitRepo { path }
} }
/// Gets the path of the repository. // Check for local changes
/// if has_git_changes(actual_path)? {
/// # Returns return Err(GitError::LocalChangesExist(actual_path.clone()));
///
/// * The path to the git repository
pub fn path(&self) -> &str {
&self.path
} }
/// Checks if the repository has uncommitted changes. // Pull the latest changes
/// let output = Command::new("git")
/// # Returns .args(&["-C", actual_path, "pull"])
/// .output()
/// * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise .map_err(GitError::CommandExecutionError)?;
/// * `Err(GitError)` - If the operation failed
pub fn has_changes(&self) -> Result<bool, GitError> {
let output = Command::new("git")
.args(&["-C", &self.path, "status", "--porcelain"])
.output()
.map_err(GitError::CommandExecutionError)?;
Ok(!output.stdout.is_empty()) if output.status.success() {
} let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("Already up to date") {
/// Pulls the latest changes from the remote repository. Ok(format!("Repository already up to date at {}", actual_path))
///
/// # Returns
///
/// * `Ok(Self)` - The GitRepo object for method chaining
/// * `Err(GitError)` - If the pull operation failed
pub fn pull(&self) -> Result<Self, GitError> {
// Check if repository exists and is a git repository
let git_dir = Path::new(&self.path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(self.path.clone()));
}
// Check for local changes
if self.has_changes()? {
return Err(GitError::LocalChangesExist(self.path.clone()));
}
// Pull the latest changes
let output = Command::new("git")
.args(&["-C", &self.path, "pull"])
.output()
.map_err(GitError::CommandExecutionError)?;
if output.status.success() {
Ok(self.clone())
} else { } else {
let error = String::from_utf8_lossy(&output.stderr); Ok(format!("Successfully updated repository at {}", actual_path))
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
}
}
/// Resets any local changes in the repository.
///
/// # Returns
///
/// * `Ok(Self)` - The GitRepo object for method chaining
/// * `Err(GitError)` - If the reset operation failed
pub fn reset(&self) -> Result<Self, GitError> {
// Check if repository exists and is a git repository
let git_dir = Path::new(&self.path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(self.path.clone()));
}
// Reset any local changes
let reset_output = Command::new("git")
.args(&["-C", &self.path, "reset", "--hard", "HEAD"])
.output()
.map_err(GitError::CommandExecutionError)?;
if !reset_output.status.success() {
let error = String::from_utf8_lossy(&reset_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
}
// Clean untracked files
let clean_output = Command::new("git")
.args(&["-C", &self.path, "clean", "-fd"])
.output()
.map_err(GitError::CommandExecutionError)?;
if !clean_output.status.success() {
let error = String::from_utf8_lossy(&clean_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
}
Ok(self.clone())
}
/// Commits changes in the repository.
///
/// # Arguments
///
/// * `message` - The commit message
///
/// # Returns
///
/// * `Ok(Self)` - The GitRepo object for method chaining
/// * `Err(GitError)` - If the commit operation failed
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
// Check if repository exists and is a git repository
let git_dir = Path::new(&self.path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(self.path.clone()));
}
// Check for local changes
if !self.has_changes()? {
return Ok(self.clone());
}
// Add all changes
let add_output = Command::new("git")
.args(&["-C", &self.path, "add", "."])
.output()
.map_err(GitError::CommandExecutionError)?;
if !add_output.status.success() {
let error = String::from_utf8_lossy(&add_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
}
// Commit the changes
let commit_output = Command::new("git")
.args(&["-C", &self.path, "commit", "-m", message])
.output()
.map_err(GitError::CommandExecutionError)?;
if !commit_output.status.success() {
let error = String::from_utf8_lossy(&commit_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
}
Ok(self.clone())
}
/// Pushes changes to the remote repository.
///
/// # Returns
///
/// * `Ok(Self)` - The GitRepo object for method chaining
/// * `Err(GitError)` - If the push operation failed
pub fn push(&self) -> Result<Self, GitError> {
// Check if repository exists and is a git repository
let git_dir = Path::new(&self.path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(self.path.clone()));
}
// Push the changes
let push_output = Command::new("git")
.args(&["-C", &self.path, "push"])
.output()
.map_err(GitError::CommandExecutionError)?;
if push_output.status.success() {
Ok(self.clone())
} else {
let error = String::from_utf8_lossy(&push_output.stderr);
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
} }
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
} }
} }
// Implement Clone for GitRepo to allow for method chaining /**
impl Clone for GitRepo { * Force updates a git repository by discarding local changes and pulling the latest changes.
fn clone(&self) -> Self { *
GitRepo { * This function will reset any uncommitted changes and clean untracked files before pulling.
path: self.path.clone(), *
} * # Arguments
*
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
*
* # Returns
*
* * `Ok(String)` - A success message indicating the repository was force-updated
* * `Err(GitError)` - An error if the update failed
*
* # Examples
*
* ```
* let result = git_update_force("my-project")?;
* println!("{}", result); // "Successfully force-updated repository at /home/user/code/github.com/user/my-project"
* ```
*/
pub fn git_update_force(repo_path: &str) -> Result<String, GitError> {
// If repo_path may be a partial path, find the matching repository
let repos = find_matching_repos(repo_path)?;
// Should only be one repository at this point
let actual_path = &repos[0];
// Check if repository exists and is a git repository
let git_dir = Path::new(actual_path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(actual_path.clone()));
}
// Reset any local changes
let reset_output = Command::new("git")
.args(&["-C", actual_path, "reset", "--hard", "HEAD"])
.output()
.map_err(GitError::CommandExecutionError)?;
if !reset_output.status.success() {
let error = String::from_utf8_lossy(&reset_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
}
// Clean untracked files
let clean_output = Command::new("git")
.args(&["-C", actual_path, "clean", "-fd"])
.output()
.map_err(GitError::CommandExecutionError)?;
if !clean_output.status.success() {
let error = String::from_utf8_lossy(&clean_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
}
// Pull the latest changes
let pull_output = Command::new("git")
.args(&["-C", actual_path, "pull"])
.output()
.map_err(GitError::CommandExecutionError)?;
if pull_output.status.success() {
Ok(format!("Successfully force-updated repository at {}", actual_path))
} else {
let error = String::from_utf8_lossy(&pull_output.stderr);
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
}
}
/**
* Commits changes in a git repository and then updates it by pulling the latest changes.
*
* # Arguments
*
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
* * `message` - The commit message
*
* # Returns
*
* * `Ok(String)` - A success message indicating the repository was committed and updated
* * `Err(GitError)` - An error if the operation failed
*
* # Examples
*
* ```
* let result = git_update_commit("my-project", "Fix bug in login form")?;
* println!("{}", result); // "Successfully committed and updated repository at /home/user/code/github.com/user/my-project"
* ```
*/
pub fn git_update_commit(repo_path: &str, message: &str) -> Result<String, GitError> {
// If repo_path may be a partial path, find the matching repository
let repos = find_matching_repos(repo_path)?;
// Should only be one repository at this point
let actual_path = &repos[0];
// Check if repository exists and is a git repository
let git_dir = Path::new(actual_path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(actual_path.clone()));
}
// Check for local changes
if !has_git_changes(actual_path)? {
return Ok(format!("No changes to commit in repository at {}", actual_path));
}
// Add all changes
let add_output = Command::new("git")
.args(&["-C", actual_path, "add", "."])
.output()
.map_err(GitError::CommandExecutionError)?;
if !add_output.status.success() {
let error = String::from_utf8_lossy(&add_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
}
// Commit the changes
let commit_output = Command::new("git")
.args(&["-C", actual_path, "commit", "-m", message])
.output()
.map_err(GitError::CommandExecutionError)?;
if !commit_output.status.success() {
let error = String::from_utf8_lossy(&commit_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
}
// Pull the latest changes
let pull_output = Command::new("git")
.args(&["-C", actual_path, "pull"])
.output()
.map_err(GitError::CommandExecutionError)?;
if pull_output.status.success() {
Ok(format!("Successfully committed and updated repository at {}", actual_path))
} else {
let error = String::from_utf8_lossy(&pull_output.stderr);
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
}
}
/**
* Commits changes in a git repository and pushes them to the remote.
*
* # Arguments
*
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
* * `message` - The commit message
*
* # Returns
*
* * `Ok(String)` - A success message indicating the repository was committed and pushed
* * `Err(GitError)` - An error if the operation failed
*
* # Examples
*
* ```
* let result = git_update_commit_push("my-project", "Add new feature")?;
* println!("{}", result); // "Successfully committed and pushed repository at /home/user/code/github.com/user/my-project"
* ```
*/
pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result<String, GitError> {
// If repo_path may be a partial path, find the matching repository
let repos = find_matching_repos(repo_path)?;
// Should only be one repository at this point
let actual_path = &repos[0];
// Check if repository exists and is a git repository
let git_dir = Path::new(actual_path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(actual_path.clone()));
}
// Check for local changes
if !has_git_changes(actual_path)? {
return Ok(format!("No changes to commit in repository at {}", actual_path));
}
// Add all changes
let add_output = Command::new("git")
.args(&["-C", actual_path, "add", "."])
.output()
.map_err(GitError::CommandExecutionError)?;
if !add_output.status.success() {
let error = String::from_utf8_lossy(&add_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
}
// Commit the changes
let commit_output = Command::new("git")
.args(&["-C", actual_path, "commit", "-m", message])
.output()
.map_err(GitError::CommandExecutionError)?;
if !commit_output.status.success() {
let error = String::from_utf8_lossy(&commit_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
}
// Push the changes
let push_output = Command::new("git")
.args(&["-C", actual_path, "push"])
.output()
.map_err(GitError::CommandExecutionError)?;
if push_output.status.success() {
Ok(format!("Successfully committed and pushed repository at {}", actual_path))
} else {
let error = String::from_utf8_lossy(&push_output.stderr);
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
} }
} }

View File

@ -160,7 +160,7 @@ impl GitExecutor {
// Get authentication configuration for a git URL // Get authentication configuration for a git URL
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> { fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
if let Some(config) = &self.config { if let Some(config) = &self.config {
let (server, _, _) = crate::git::git::parse_git_url(url); let (server, _, _) = parse_git_url(url);
if !server.is_empty() { if !server.is_empty() {
return config.auth.get(&server); return config.auth.get(&server);
} }

View File

@ -1,212 +0,0 @@
# Git Interface Redesign Plan
## Current Understanding
The current git interface consists of standalone functions like `git_clone`, `git_list`, `git_update`, etc. We want to replace this with an object-oriented interface using a builder pattern that allows for method chaining.
## New Interface Design
### Core Components
```mermaid
classDiagram
class GitTree {
+String base_path
+new(base_path: &str) Result<GitTree, GitError>
+list() Result<Vec<String>, GitError>
+find(pattern: &str) Result<Vec<String>, GitError>
+get(path_pattern: &str) Result<Vec<GitRepo>, GitError>
}
class GitRepo {
+String path
+pull() Result<GitRepo, GitError>
+reset() Result<GitRepo, GitError>
+push() Result<GitRepo, GitError>
+commit(message: &str) Result<GitRepo, GitError>
+has_changes() Result<bool, GitError>
}
GitTree --> GitRepo : creates
```
### Implementation Details
1. **GitTree Class**:
- Constructor takes a base path parameter that specifies where all git repositories will be located
- Methods for listing and finding repositories
- A `get()` method that returns one or more GitRepo objects based on a path pattern
- The `get()` method can also accept a URL (git or http format) and will clone the repository if it doesn't exist
2. **GitRepo Class**:
- Represents a single git repository
- Methods for common git operations: pull, reset, push, commit
- Each method returns a Result containing either the GitRepo object (for chaining) or an error
- If an operation fails, subsequent operations in the chain are skipped
3. **Error Handling**:
- Each method returns a Result type for immediate error handling
- Errors are propagated up the call chain
- The existing GitError enum will be reused
## Implementation Plan
### 1. Create the GitTree and GitRepo Structs in git.rs
```rust
pub struct GitTree {
base_path: String,
}
pub struct GitRepo {
path: String,
}
```
### 2. Implement the GitTree Methods
```rust
impl GitTree {
pub fn new(base_path: &str) -> Result<Self, GitError> {
// Validate the base path
// Create the directory if it doesn't exist
Ok(GitTree {
base_path: base_path.to_string(),
})
}
pub fn list(&self) -> Result<Vec<String>, GitError> {
// List all git repositories under the base path
}
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> {
// Find repositories matching the pattern
}
pub fn get(&self, path_pattern: &str) -> Result<Vec<GitRepo>, GitError> {
// Find repositories matching the pattern
// Return GitRepo objects for each match
}
}
```
### 3. Implement the GitRepo Methods
```rust
impl GitRepo {
pub fn pull(&self) -> Result<Self, GitError> {
// Pull the latest changes
// Return self for chaining or an error
}
pub fn reset(&self) -> Result<Self, GitError> {
// Reset any local changes
// Return self for chaining or an error
}
pub fn push(&self) -> Result<Self, GitError> {
// Push changes to the remote
// Return self for chaining or an error
}
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
// Commit changes with the given message
// Return self for chaining or an error
}
pub fn has_changes(&self) -> Result<bool, GitError> {
// Check if the repository has uncommitted changes
}
}
```
### 4. Update the Rhai Wrappers in rhai/git.rs
```rust
// Register the GitTree and GitRepo types with Rhai
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register the GitTree type
engine.register_type::<GitTree>();
engine.register_fn("new", git_tree_new);
// Register GitTree methods
engine.register_fn("list", git_tree_list);
engine.register_fn("find", git_tree_find);
engine.register_fn("get", git_tree_get);
// Register GitRepo methods
engine.register_type::<GitRepo>();
engine.register_fn("pull", git_repo_pull);
engine.register_fn("reset", git_repo_reset);
engine.register_fn("push", git_repo_push);
engine.register_fn("commit", git_repo_commit);
engine.register_fn("has_changes", git_repo_has_changes);
Ok(())
}
```
### 5. Update Tests and Examples
- Update the test files to use the new interface
- Create new examples demonstrating the builder pattern and method chaining
## Usage Examples
### Example 1: Basic Repository Operations
```rhai
// Create a new GitTree object
let git_tree = new("/home/user/code");
// List all repositories
let repos = git_tree.list();
print(`Found ${repos.len()} repositories`);
// Find repositories matching a pattern
let matching = git_tree.find("my-project*");
print(`Found ${matching.len()} matching repositories`);
// Get a repository and perform operations
let repo = git_tree.get("my-project")[0];
let result = repo.pull().reset().commit("Update files").push();
```
### Example 2: Working with Multiple Repositories
```rhai
// Create a new GitTree object
let git_tree = new("/home/user/code");
// Get all repositories matching a pattern
let repos = git_tree.get("project*");
print(`Found ${repos.len()} matching repositories`);
// Perform operations on all repositories
for repo in repos {
let result = repo.pull();
if result.is_ok() {
print(`Successfully pulled ${repo.path}`);
} else {
print(`Failed to pull ${repo.path}: ${result.error}`);
}
}
```
### Example 3: Cloning a Repository
```rhai
// Create a new GitTree object
let git_tree = new("/home/user/code");
// Clone a repository by URL
let repos = git_tree.get("https://github.com/username/repo.git");
let repo = repos[0];
print(`Repository cloned to: ${repo.path}`);
```
## Migration Strategy
1. Implement the new interface in git.rs and rhai/git.rs
2. Update all tests and examples to use the new interface
3. Remove the old standalone functions

View File

@ -98,35 +98,16 @@ fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfil
let (ext, interpreter) = (".bat", "cmd.exe".to_string()); let (ext, interpreter) = (".bat", "cmd.exe".to_string());
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
let (ext, interpreter) = (".sh", "/bin/bash".to_string()); let (ext, interpreter) = (".sh", "/bin/sh".to_string());
// Create the script file // Create the script file
let script_path = temp_dir.path().join(format!("script{}", ext)); let script_path = temp_dir.path().join(format!("script{}", ext));
let mut file = File::create(&script_path) let mut file = File::create(&script_path)
.map_err(RunError::FileCreationFailed)?; .map_err(RunError::FileCreationFailed)?;
// For Unix systems, ensure the script has a shebang line with -e flag // Write the script content
#[cfg(any(target_os = "macos", target_os = "linux"))] file.write_all(dedented.as_bytes())
{ .map_err(RunError::FileWriteFailed)?;
let script_with_shebang = if dedented.trim_start().starts_with("#!") {
// Script already has a shebang, use it as is
dedented
} else {
// Add shebang with -e flag to ensure script fails on errors
format!("#!/bin/bash -e\n{}", dedented)
};
// Write the script content with shebang
file.write_all(script_with_shebang.as_bytes())
.map_err(RunError::FileWriteFailed)?;
}
// For Windows, just write the script as is
#[cfg(target_os = "windows")]
{
file.write_all(dedented.as_bytes())
.map_err(RunError::FileWriteFailed)?;
}
// Make the script executable (Unix only) // Make the script executable (Unix only)
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
@ -185,8 +166,7 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
if let Ok(l) = line { if let Ok(l) = line {
// Print the line if not silent and flush immediately // Print the line if not silent and flush immediately
if !silent_clone { if !silent_clone {
// Always print stderr, even if silent is true, for error visibility eprintln!("{}", l);
eprintln!("\x1b[31mERROR: {}\x1b[0m", l); // Red color for errors
std::io::stderr().flush().unwrap_or(()); std::io::stderr().flush().unwrap_or(());
} }
// Store it in our captured buffer // Store it in our captured buffer
@ -218,14 +198,6 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
"Failed to capture stderr".to_string() "Failed to capture stderr".to_string()
}; };
// If the command failed, print the stderr if it wasn't already printed
if !status.success() && silent && !captured_stderr.is_empty() {
eprintln!("\x1b[31mCommand failed with error:\x1b[0m");
for line in captured_stderr.lines() {
eprintln!("\x1b[31m{}\x1b[0m", line);
}
}
// Return the command result // Return the command result
Ok(CommandResult { Ok(CommandResult {
stdout: captured_stdout, stdout: captured_stdout,
@ -242,20 +214,6 @@ fn process_command_output(output: Result<Output, std::io::Error>) -> Result<Comm
let stdout = String::from_utf8_lossy(&out.stdout).to_string(); let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string(); let stderr = String::from_utf8_lossy(&out.stderr).to_string();
// Print stderr if there's any, even for silent execution
if !stderr.is_empty() {
eprintln!("\x1b[31mCommand stderr output:\x1b[0m");
for line in stderr.lines() {
eprintln!("\x1b[31m{}\x1b[0m", line);
}
}
// If the command failed, print a clear error message
if !out.status.success() {
eprintln!("\x1b[31mCommand failed with exit code: {}\x1b[0m",
out.status.code().unwrap_or(-1));
}
Ok(CommandResult { Ok(CommandResult {
stdout, stdout,
stderr, stderr,
@ -302,18 +260,7 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
.args(&command_args) .args(&command_args)
.output(); .output();
let result = process_command_output(output)?; process_command_output(output)
// If the script failed, return an error
if !result.success {
return Err(RunError::CommandFailed(format!(
"Script execution failed with exit code {}: {}",
result.code,
result.stderr.trim()
)));
}
Ok(result)
} else { } else {
// For normal execution, spawn and handle the output streams // For normal execution, spawn and handle the output streams
let child = Command::new(interpreter) let child = Command::new(interpreter)
@ -323,45 +270,16 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
.spawn() .spawn()
.map_err(RunError::CommandExecutionFailed)?; .map_err(RunError::CommandExecutionFailed)?;
let result = handle_child_output(child, false)?; handle_child_output(child, false)
// If the script failed, return an error
if !result.success {
return Err(RunError::CommandFailed(format!(
"Script execution failed with exit code {}: {}",
result.code,
result.stderr.trim()
)));
}
Ok(result)
} }
} }
/// Run a multiline script with optional silent mode /// Run a multiline script with optional silent mode
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> { fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
// Print the script being executed if not silent
if !silent {
println!("\x1b[36mExecuting script:\x1b[0m");
for (i, line) in script.lines().enumerate() {
println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
}
println!("\x1b[36m---\x1b[0m");
}
let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?; let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
// _temp_dir is kept in scope until the end of this function to ensure // _temp_dir is kept in scope until the end of this function to ensure
// it's not dropped prematurely, which would clean up the directory // it's not dropped prematurely, which would clean up the directory
execute_script_internal(&interpreter, &script_path, silent)
// Execute the script and handle the result
let result = execute_script_internal(&interpreter, &script_path, silent);
// If there was an error, print a clear error message
if let Err(ref e) = result {
eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e);
}
result
} }
/// A builder for configuring and executing commands or scripts /// A builder for configuring and executing commands or scripts
@ -420,41 +338,21 @@ impl<'a> RunBuilder<'a> {
// Log command execution if enabled // Log command execution if enabled
if self.log { if self.log {
println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed); println!("[LOG] Executing command: {}", trimmed);
} }
// Handle async execution // Handle async execution
if self.async_exec { if self.async_exec {
let cmd_copy = trimmed.to_string(); let cmd_copy = trimmed.to_string();
let silent = self.silent; let silent = self.silent;
let log = self.log;
// Spawn a thread to run the command asynchronously // Spawn a thread to run the command asynchronously
thread::spawn(move || { thread::spawn(move || {
if log { let _ = if cmd_copy.contains('\n') {
println!("\x1b[36m[ASYNC] Starting execution\x1b[0m");
}
let result = if cmd_copy.contains('\n') {
run_script_internal(&cmd_copy, silent) run_script_internal(&cmd_copy, silent)
} else { } else {
run_command_internal(&cmd_copy, silent) run_command_internal(&cmd_copy, silent)
}; };
if log {
match &result {
Ok(res) => {
if res.success {
println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
} else {
eprintln!("\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m", res.code);
}
},
Err(e) => {
eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
}
}
}
}); });
// Return a placeholder result for async execution // Return a placeholder result for async execution
@ -477,17 +375,8 @@ impl<'a> RunBuilder<'a> {
// Handle die=false: convert errors to CommandResult with success=false // Handle die=false: convert errors to CommandResult with success=false
match result { match result {
Ok(res) => { Ok(res) => Ok(res),
// If the command failed but die is false, print a warning
if !res.success && !self.die && !self.silent {
eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
}
Ok(res)
},
Err(e) => { Err(e) => {
// Always print the error, even if die is false
eprintln!("\x1b[31mCommand error: {}\x1b[0m", e);
if self.die { if self.die {
Err(e) Err(e)
} else { } else {

View File

@ -4,7 +4,7 @@
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map}; use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
use std::collections::HashMap; use std::collections::HashMap;
use crate::virt::buildah::{self, BuildahError, Image, Builder}; use crate::virt::buildah::{self, BuildahError, Image};
use crate::process::CommandResult; use crate::process::CommandResult;
/// Register Buildah module functions with the Rhai engine /// Register Buildah module functions with the Rhai engine
@ -20,42 +20,35 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
// Register types // Register types
register_bah_types(engine)?; register_bah_types(engine)?;
// Register Builder constructor // Register container functions
engine.register_fn("bah_new", bah_new); engine.register_fn("bah_from", bah_from);
engine.register_fn("bah_run", bah_run);
engine.register_fn("bah_run_with_isolation", bah_run_with_isolation);
engine.register_fn("bah_copy", bah_copy);
engine.register_fn("bah_add", bah_add);
engine.register_fn("bah_commit", bah_commit);
engine.register_fn("bah_remove", bah_remove);
engine.register_fn("bah_list", bah_list);
engine.register_fn("bah_build", bah_build_with_options);
engine.register_fn("bah_new_build_options", new_build_options);
// Register Builder instance methods // Register image functions
engine.register_fn("run", builder_run); engine.register_fn("bah_images", images);
engine.register_fn("run_with_isolation", builder_run_with_isolation); engine.register_fn("bah_image_remove", image_remove);
engine.register_fn("copy", builder_copy); engine.register_fn("bah_image_push", image_push);
engine.register_fn("add", builder_add); engine.register_fn("bah_image_tag", image_tag);
engine.register_fn("commit", builder_commit); engine.register_fn("bah_image_pull", image_pull);
// Remove the line that's causing the error engine.register_fn("bah_image_commit", image_commit_with_options);
engine.register_fn("remove", builder_remove); engine.register_fn("bah_new_commit_options", new_commit_options);
engine.register_fn("reset", builder_reset); engine.register_fn("bah_config", config_with_options);
engine.register_fn("config", builder_config); engine.register_fn("bah_new_config_options", new_config_options);
// Register Builder static methods
engine.register_fn("images", builder_images);
engine.register_fn("image_remove", builder_image_remove);
engine.register_fn("image_pull", builder_image_pull);
engine.register_fn("image_push", builder_image_push);
engine.register_fn("image_tag", builder_image_tag);
engine.register_fn("build", builder_build);
Ok(()) Ok(())
} }
/// Register Buildah module types with the Rhai engine /// Register Buildah module types with the Rhai engine
fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register Builder type // Register Image type and methods
engine.register_type_with_name::<Builder>("BuildahBuilder");
// Register getters for Builder properties
engine.register_get("container_id", get_builder_container_id);
engine.register_get("name", get_builder_name);
engine.register_get("image", get_builder_image);
// Register Image type and methods (same as before)
engine.register_type_with_name::<Image>("BuildahImage"); engine.register_type_with_name::<Image>("BuildahImage");
// Register getters for Image properties // Register getters for Image properties
@ -91,8 +84,312 @@ fn bah_error_to_rhai_error<T>(result: Result<T, BuildahError>) -> Result<T, Box<
}) })
} }
// Helper function to convert Rhai Map to Rust HashMap /// Create a new Map with default build options
fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<EvalAltResult>> { pub fn new_build_options() -> Map {
let mut map = Map::new();
map.insert("tag".into(), Dynamic::UNIT);
map.insert("context_dir".into(), Dynamic::from("."));
map.insert("file".into(), Dynamic::from("Dockerfile"));
map.insert("isolation".into(), Dynamic::UNIT);
map
}
/// Create a new Map with default commit options
pub fn new_commit_options() -> Map {
let mut map = Map::new();
map.insert("format".into(), Dynamic::UNIT);
map.insert("squash".into(), Dynamic::from(false));
map.insert("rm".into(), Dynamic::from(false));
map
}
/// Create a new Map for config options
pub fn new_config_options() -> Map {
Map::new()
}
//
// Container Function Wrappers
//
/// Wrapper for buildah::from
///
/// Create a container from an image.
pub fn bah_from(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
let result = bah_error_to_rhai_error(buildah::from(image))?;
// Create a new CommandResult with trimmed stdout
let trimmed_result = CommandResult {
stdout: result.stdout.trim().to_string(),
stderr: result.stderr.trim().to_string(),
success: result.success,
code: result.code,
};
Ok(trimmed_result)
}
/// Wrapper for buildah::run
///
/// Run a command in a container.
pub fn bah_run(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::run(container, command))
}
/// Wrapper for buildah::run_with_isolation
///
/// Run a command in a container with specified isolation.
pub fn bah_run_with_isolation(container: &str, command: &str, isolation: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::bah_run_with_isolation(container, command, isolation))
}
/// Wrapper for buildah::copy
///
/// Copy files into a container.
pub fn bah_copy(container: &str, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::bah_copy(container, source, dest))
}
/// Wrapper for buildah::add
///
/// Add files into a container.
pub fn bah_add(container: &str, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::bah_add(container, source, dest))
}
/// Wrapper for buildah::commit
///
/// Commit a container to an image.
pub fn bah_commit(container: &str, image_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::bah_commit(container, image_name))
}
/// Wrapper for buildah::remove
///
/// Remove a container.
pub fn bah_remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::bah_remove(container))
}
/// Wrapper for buildah::list
///
/// List containers.
pub fn bah_list() -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::bah_list())
}
/// Build an image with options specified in a Map
///
/// This provides a builder-style interface for Rhai scripts.
///
/// # Example
///
/// ```rhai
/// let options = bah_new_build_options();
/// options.tag = "my-image:latest";
/// options.context_dir = ".";
/// options.file = "Dockerfile";
/// options.isolation = "chroot";
/// let result = bah_build(options);
/// ```
pub fn bah_build_with_options(options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
// Extract options from the map
let tag_option = match options.get("tag") {
Some(tag) => {
if tag.is_unit() {
None
} else if let Ok(tag_str) = tag.clone().into_string() {
Some(tag_str)
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"tag must be a string".into(),
rhai::Position::NONE
)));
}
},
None => None
};
let context_dir = match options.get("context_dir") {
Some(dir) => {
if let Ok(dir_str) = dir.clone().into_string() {
dir_str
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"context_dir must be a string".into(),
rhai::Position::NONE
)));
}
},
None => String::from(".")
};
let file = match options.get("file") {
Some(file) => {
if let Ok(file_str) = file.clone().into_string() {
file_str
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"file must be a string".into(),
rhai::Position::NONE
)));
}
},
None => String::from("Dockerfile")
};
let isolation_option = match options.get("isolation") {
Some(isolation) => {
if isolation.is_unit() {
None
} else if let Ok(isolation_str) = isolation.clone().into_string() {
Some(isolation_str)
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"isolation must be a string".into(),
rhai::Position::NONE
)));
}
},
None => None
};
// Convert String to &str for the function call
let tag_ref = tag_option.as_deref();
let isolation_ref = isolation_option.as_deref();
// Call the buildah build function
bah_error_to_rhai_error(buildah::bah_build(tag_ref, &context_dir, &file, isolation_ref))
}
//
// Image Function Wrappers
//
/// Wrapper for buildah::images
///
/// List images in local storage.
pub fn images() -> Result<Array, Box<EvalAltResult>> {
let images = bah_error_to_rhai_error(buildah::images())?;
// Convert Vec<Image> to Rhai Array
let mut array = Array::new();
for image in images {
array.push(Dynamic::from(image));
}
Ok(array)
}
/// Wrapper for buildah::image_remove
///
/// Remove one or more images.
pub fn image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::image_remove(image))
}
/// Wrapper for buildah::image_push
///
/// Push an image to a registry.
pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::image_push(image, destination, tls_verify))
}
/// Wrapper for buildah::image_tag
///
/// Add an additional name to a local image.
pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::image_tag(image, new_name))
}
/// Wrapper for buildah::image_pull
///
/// Pull an image from a registry.
pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(buildah::image_pull(image, tls_verify))
}
/// Commit a container to an image with options specified in a Map
///
/// This provides a builder-style interface for Rhai scripts.
///
/// # Example
///
/// ```rhai
/// let options = bah_new_commit_options();
/// options.format = "docker";
/// options.squash = true;
/// options.rm = true;
/// let result = bah_image_commit("my-container", "my-image:latest", options);
/// ```
pub fn image_commit_with_options(container: &str, image_name: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
// Extract options from the map
let format_option = match options.get("format") {
Some(format) => {
if format.is_unit() {
None
} else if let Ok(format_str) = format.clone().into_string() {
Some(format_str)
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"format must be a string".into(),
rhai::Position::NONE
)));
}
},
None => None
};
let squash = match options.get("squash") {
Some(squash) => {
if let Ok(squash_val) = squash.clone().as_bool() {
squash_val
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"squash must be a boolean".into(),
rhai::Position::NONE
)));
}
},
None => false
};
let rm = match options.get("rm") {
Some(rm) => {
if let Ok(rm_val) = rm.clone().as_bool() {
rm_val
} else {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"rm must be a boolean".into(),
rhai::Position::NONE
)));
}
},
None => false
};
// Convert String to &str for the function call
let format_ref = format_option.as_deref();
// Call the buildah image_commit function
bah_error_to_rhai_error(buildah::image_commit(container, image_name, format_ref, squash, rm))
}
/// Configure a container with options specified in a Map
///
/// This provides a builder-style interface for Rhai scripts.
///
/// # Example
///
/// ```rhai
/// let options = bah_new_config_options();
/// options.author = "John Doe";
/// options.cmd = "echo Hello";
/// options.entrypoint = "/bin/sh -c";
/// let result = bah_config("my-container", options);
/// ```
pub fn config_with_options(container: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
// Convert Rhai Map to Rust HashMap
let mut config_options = HashMap::<String, String>::new(); let mut config_options = HashMap::<String, String>::new();
for (key, value) in options.iter() { for (key, value) in options.iter() {
@ -107,96 +404,6 @@ fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<E
} }
} }
Ok(config_options) // Call the buildah config function
} bah_error_to_rhai_error(buildah::bah_config(container, 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))
}
// Builder instance methods
pub fn builder_run(builder: &mut Builder, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(builder.run(command))
}
pub fn builder_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))
}
pub fn builder_copy(builder: &mut Builder, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(builder.copy(source, dest))
}
pub fn builder_add(builder: &mut Builder, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(builder.add(source, dest))
}
pub fn builder_commit(builder: &mut Builder, image_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(builder.commit(image_name))
}
pub fn builder_remove(builder: &mut Builder) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(builder.remove())
}
pub fn builder_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))
}
// Builder static methods
pub fn builder_images(_builder: &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)
}
pub fn builder_image_remove(_builder: &mut Builder, image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(Builder::image_remove(image))
}
pub fn builder_image_pull(_builder: &mut Builder, image: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(Builder::image_pull(image, tls_verify))
}
pub fn builder_image_push(_builder: &mut Builder, image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify))
}
pub fn builder_image_tag(_builder: &mut Builder, image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(Builder::image_tag(image, new_name))
}
// Getter functions for Builder properties
pub fn get_builder_container_id(builder: &mut Builder) -> String {
match builder.container_id() {
Some(id) => id.clone(),
None => "".to_string(),
}
}
pub fn get_builder_name(builder: &mut Builder) -> String {
builder.name().to_string()
}
pub fn get_builder_image(builder: &mut Builder) -> String {
builder.image().to_string()
}
// Reset function for Builder
pub fn builder_reset(builder: &mut Builder) -> Result<(), Box<EvalAltResult>> {
bah_error_to_rhai_error(builder.reset())
}
// Build function for Builder
pub fn builder_build(_builder: &mut Builder, tag: &str, context_dir: &str, file: &str, isolation: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(Builder::build(Some(tag), context_dir, file, Some(isolation)))
} }

View File

@ -3,7 +3,7 @@
//! This module provides Rhai wrappers for the functions in the Git module. //! This module provides Rhai wrappers for the functions in the Git module.
use rhai::{Engine, EvalAltResult, Array, Dynamic}; use rhai::{Engine, EvalAltResult, Array, Dynamic};
use crate::git::{GitTree, GitRepo, GitError}; use crate::git::{self, GitError};
/// Register Git module functions with the Rhai engine /// Register Git module functions with the Rhai engine
/// ///
@ -15,21 +15,15 @@ use crate::git::{GitTree, GitRepo, GitError};
/// ///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise /// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register GitTree constructor // Register basic git functions
engine.register_fn("gittree_new", git_tree_new); engine.register_fn("git_clone", git_clone);
engine.register_fn("git_list", git_list);
// Register GitTree methods engine.register_fn("git_update", git_update);
engine.register_fn("list", git_tree_list); engine.register_fn("git_update_force", git_update_force);
engine.register_fn("find", git_tree_find); engine.register_fn("git_update_commit", git_update_commit);
engine.register_fn("get", git_tree_get); engine.register_fn("git_update_commit_push", git_update_commit_push);
engine.register_fn("git_has_changes", has_git_changes);
// Register GitRepo methods engine.register_fn("git_find_repos", find_matching_repos);
engine.register_fn("path", git_repo_path);
engine.register_fn("has_changes", git_repo_has_changes);
engine.register_fn("pull", git_repo_pull);
engine.register_fn("reset", git_repo_reset);
engine.register_fn("commit", git_repo_commit);
engine.register_fn("push", git_repo_push);
Ok(()) Ok(())
} }
@ -45,21 +39,21 @@ fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<Eval
} }
// //
// GitTree Function Wrappers // Git Function Wrappers
// //
/// Wrapper for GitTree::new /// Wrapper for git::git_clone
/// ///
/// Creates a new GitTree with the specified base path. /// Clones a git repository to a standardized location in the user's home directory.
pub fn git_tree_new(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> { pub fn git_clone(url: &str) -> Result<String, Box<EvalAltResult>> {
git_error_to_rhai_error(GitTree::new(base_path)) git_error_to_rhai_error(git::git_clone(url))
} }
/// Wrapper for GitTree::list /// Wrapper for git::git_list
/// ///
/// Lists all git repositories under the base path. /// Lists all git repositories found in the user's ~/code directory.
pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> { pub fn git_list() -> Result<Array, Box<EvalAltResult>> {
let repos = git_error_to_rhai_error(git_tree.list())?; let repos = git_error_to_rhai_error(git::git_list())?;
// Convert Vec<String> to Rhai Array // Convert Vec<String> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
@ -70,11 +64,18 @@ pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>
Ok(array) Ok(array)
} }
/// Wrapper for GitTree::find /// Wrapper for git::has_git_changes
///
/// Checks if a git repository has uncommitted changes.
pub fn has_git_changes(repo_path: &str) -> Result<bool, Box<EvalAltResult>> {
git_error_to_rhai_error(git::has_git_changes(repo_path))
}
/// Wrapper for git::find_matching_repos
/// ///
/// Finds repositories matching a pattern or partial path. /// Finds repositories matching a pattern or partial path.
pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> { pub fn find_matching_repos(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
let repos = git_error_to_rhai_error(git_tree.find(pattern))?; let repos = git_error_to_rhai_error(git::find_matching_repos(pattern))?;
// Convert Vec<String> to Rhai Array // Convert Vec<String> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
@ -85,63 +86,30 @@ pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box
Ok(array) Ok(array)
} }
/// Wrapper for GitTree::get /// Wrapper for git::git_update
/// ///
/// Gets one or more GitRepo objects based on a path pattern or URL. /// Updates a git repository by pulling the latest changes.
pub fn git_tree_get(git_tree: &mut GitTree, path_or_url: &str) -> Result<Array, Box<EvalAltResult>> { pub fn git_update(repo_path: &str) -> Result<String, Box<EvalAltResult>> {
let repos = git_error_to_rhai_error(git_tree.get(path_or_url))?; git_error_to_rhai_error(git::git_update(repo_path))
// Convert Vec<GitRepo> to Rhai Array
let mut array = Array::new();
for repo in repos {
array.push(Dynamic::from(repo));
}
Ok(array)
} }
// /// Wrapper for git::git_update_force
// GitRepo Function Wrappers
//
/// Wrapper for GitRepo::path
/// ///
/// Gets the path of the repository. /// Force updates a git repository by discarding local changes and pulling the latest changes.
pub fn git_repo_path(git_repo: &mut GitRepo) -> String { pub fn git_update_force(repo_path: &str) -> Result<String, Box<EvalAltResult>> {
git_repo.path().to_string() git_error_to_rhai_error(git::git_update_force(repo_path))
} }
/// Wrapper for GitRepo::has_changes /// Wrapper for git::git_update_commit
/// ///
/// Checks if the repository has uncommitted changes. /// Commits changes in a git repository and then updates it by pulling the latest changes.
pub fn git_repo_has_changes(git_repo: &mut GitRepo) -> Result<bool, Box<EvalAltResult>> { pub fn git_update_commit(repo_path: &str, message: &str) -> Result<String, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.has_changes()) git_error_to_rhai_error(git::git_update_commit(repo_path, message))
} }
/// Wrapper for GitRepo::pull /// Wrapper for git::git_update_commit_push
/// ///
/// Pulls the latest changes from the remote repository. /// Commits changes in a git repository and pushes them to the remote.
pub fn git_repo_pull(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> { pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result<String, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.pull()) git_error_to_rhai_error(git::git_update_commit_push(repo_path, message))
}
/// Wrapper for GitRepo::reset
///
/// Resets any local changes in the repository.
pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.reset())
}
/// Wrapper for GitRepo::commit
///
/// Commits changes in the repository.
pub fn git_repo_commit(git_repo: &mut GitRepo, message: &str) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.commit(message))
}
/// Wrapper for GitRepo::push
///
/// Pushes changes to the remote repository.
pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.push())
} }

View File

@ -39,22 +39,25 @@ pub use process::{
// Re-export buildah functions // Re-export buildah functions
pub use buildah::register_bah_module; pub use buildah::register_bah_module;
pub use buildah::bah_new; pub use buildah::{
bah_from, bah_run, bah_run_with_isolation, bah_copy, bah_add, bah_commit,
bah_remove, bah_list, bah_build_with_options,
new_commit_options, new_config_options, image_commit_with_options, config_with_options
};
// Re-export nerdctl functions // Re-export nerdctl functions
pub use nerdctl::register_nerdctl_module; pub use nerdctl::register_nerdctl_module;
pub use nerdctl::{ pub use nerdctl::{
// Container functions nerdctl_run, nerdctl_exec,
nerdctl_run, nerdctl_run_with_name, nerdctl_run_with_port, nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list
nerdctl_exec, nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list,
// Image functions
nerdctl_images, nerdctl_image_remove, nerdctl_image_push, nerdctl_image_tag,
nerdctl_image_pull, nerdctl_image_commit, nerdctl_image_build
}; };
// Re-export git module // Re-export git functions
pub use git::register_git_module; pub use git::register_git_module;
pub use crate::git::{GitTree, GitRepo}; pub use git::{
git_clone, git_list, git_update, git_update_force, git_update_commit,
git_update_commit_push, has_git_changes, find_matching_repos
};
// Rename copy functions to avoid conflicts // Rename copy functions to avoid conflicts
pub use os::copy as os_copy; pub use os::copy as os_copy;

View File

@ -0,0 +1,131 @@
// Test script for Git module functions
// Import required modules
import "os" as os;
import "process" as process;
// Test git_clone function
fn test_git_clone() {
// Use a public repository for testing
let repo_url = "https://github.com/rhaiscript/rhai.git";
// Clone the repository
print("Testing git_clone...");
let result = git_clone(repo_url);
// Print the result
print(`Clone result: ${result}`);
// Verify the repository exists
if result.contains("already exists") {
print("Repository already exists, test passed");
return true;
}
// Check if the path exists
if exist(result) {
print("Repository cloned successfully, test passed");
return true;
}
print("Repository clone failed");
return false;
}
// Test git_list function
fn test_git_list() {
print("Testing git_list...");
let repos = git_list();
print(`Found ${repos.len()} repositories`);
// Print the first few repositories
let count = if repos.len() > 3 { 3 } else { repos.len() };
for i in range(0, count) {
print(` - ${repos[i]}`);
}
return repos.len() > 0;
}
// Test git_has_changes function
fn test_git_has_changes() {
print("Testing git_has_changes...");
// Get a repository from the list
let repos = git_list();
if repos.len() == 0 {
print("No repositories found, skipping test");
return true;
}
let repo = repos[0];
let has_changes = git_has_changes(repo);
print(`Repository ${repo} has changes: ${has_changes}`);
return true;
}
// Test find_matching_repos function
fn test_find_matching_repos() {
print("Testing find_matching_repos...");
// Get all repositories with wildcard
let all_repos = git_list();
if all_repos.len() == 0 {
print("No repositories found, skipping test");
return true;
}
// Extract a part of the first repo name to search for
let repo_name = all_repos[0].split("/").last();
let search_pattern = repo_name.substring(0, 3) + "*";
print(`Searching for repositories matching pattern: ${search_pattern}`);
let matching = find_matching_repos(search_pattern);
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
return matching.len() > 0;
}
// Run the tests
fn run_tests() {
let tests = [
#{ name: "git_clone", fn: test_git_clone },
#{ name: "git_list", fn: test_git_list },
#{ name: "git_has_changes", fn: test_git_has_changes },
#{ name: "find_matching_repos", fn: test_find_matching_repos }
];
let passed = 0;
let failed = 0;
for test in tests {
print(`\nRunning test: ${test.name}`);
let result = false;
try {
result = test.fn();
} catch(err) {
print(`Test ${test.name} threw an error: ${err}`);
result = false;
}
if result {
print(`Test ${test.name} PASSED`);
passed += 1;
} else {
print(`Test ${test.name} FAILED`);
failed += 1;
}
}
print(`\nTest summary: ${passed} passed, ${failed} failed`);
}
// Run all tests
run_tests();

View File

@ -1,106 +0,0 @@
// 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}`);
// Create a builder object
println("\nCreating a builder object:");
let container_name = "my-container-example";
// Create a new builder
let builder = bah_new(container_name, "alpine:latest");
// Reset the builder to remove any existing container
println("\nResetting the builder to start fresh:");
let reset_result = builder.reset();
println(`Reset result: ${reset_result}`);
// Create a new container after reset
println("\nCreating a new container after reset:");
builder = bah_new(container_name, "alpine:latest");
println(`Container created with ID: ${builder.container_id}`);
println(`Builder created with name: ${builder.name}, image: ${builder.image}`);
// List available images (only if buildah is installed)
println("\nListing available container images:");
// if ! buildah_exists != "" {
// //EXIT
// }
let images = builder.images();
println(`Found ${images.len()} images`);
// Print image details (limited to 3)
let count = 0;
for img in images {
if count >= 3 {
break;
}
println(` - ID: ${img.id}, Name: ${img.name}, Created: ${img.created}`);
count += 1;
}
//Run a command in the container
println("\nRunning a command in the container:");
let run_result = builder.run("echo 'Hello from container'");
println(`Command output: ${run_result.stdout}`);
//Add a file to the container
println("\nAdding a file to the container:");
let test_file = "test_file.txt";
// Create the test file using Rhai's file_write function
file_write(test_file, "Test content");
println(`Created test file: ${test_file}`);
println(`Created test file: ${test_file}`);
let add_result = builder.add(test_file, "/");
println(`Add result: ${add_result.success}`);
//Commit the container to create a new image
println("\nCommitting the container to create a new image:");
let commit_result = builder.commit("my-custom-image:latest");
println(`Commit result: ${commit_result.success}`);
//Remove the container
println("\nRemoving the container:");
let remove_result = builder.remove();
println(`Remove result: ${remove_result.success}`);
//Clean up the test file
delete(test_file);
// Demonstrate static methods
println("\nDemonstrating static methods:");
println("Building an image from a Dockerfile:");
let build_result = builder.build("example-image:latest", ".", "example_Dockerfile", "chroot");
println(`Build result: ${build_result.success}`);
// Pull an image
println("\nPulling an image:");
let pull_result = builder.image_pull("alpine:latest", true);
println(`Pull result: ${pull_result.success}`);
// Skip commit options demonstration since we removed the legacy functions
println("\nSkipping commit options demonstration (legacy functions removed)");
// Demonstrate config method
println("\nDemonstrating config method:");
// Create a new container for config demonstration
println("Creating a new container for config demonstration:");
builder = bah_new("config-demo-container", "alpine:latest");
println(`Container created with ID: ${builder.container_id}`);
let config_options = #{
"author": "Rhai Example",
"cmd": "/bin/sh -c 'echo Hello from Buildah'"
};
let config_result = builder.config(config_options);
println(`Config result: ${config_result.success}`);
// Clean up the container
println("Removing the config demo container:");
builder.remove();
"Buildah operations script completed successfully!"

View File

@ -1,164 +0,0 @@
// Simplified test script for Git module functions
// Ensure test directory exists using a bash script
fn ensure_test_dir() {
print("Ensuring test directory exists at /tmp/code");
// Create a bash script to set up the test environment
let setup_script = `#!/bin/bash -ex
rm -rf /tmp/code
mkdir -p /tmp/code
cd /tmp/code
mkdir -p myserver.com/myaccount/repogreen
mkdir -p myserver.com/myaccount/repored
cd myserver.com/myaccount/repogreen
git init
echo 'Initial test file' > test.txt
git add test.txt
git config --local user.email 'test@example.com'
git config --local user.name 'Test User'
git commit -m 'Initial commit'
cd myserver.com/myaccount/repored
git init
echo 'Initial test file' > test2.txt
git add test2.txt
git config --local user.email 'test@example.com'
git config --local user.name 'Test User'
git commit -m 'Initial commit'
//now we have 2 repos
`;
// Run the setup script
let result = run(setup_script);
if !result.success {
print("Failed to set up test directory");
print(`Error: ${result.stderr}`);
throw "Test setup failed";
}
}
// Test GitTree creation
fn test_git_tree_creation() {
print("\n=== Testing GitTree creation ===");
let git_tree = gittree_new("/tmp/code");
print(`Created GitTree with base path: /tmp/code`);
}
// Test GitTree list method
fn test_git_tree_list() {
print("\n=== Testing GitTree list method ===");
let git_tree = gittree_new("/tmp/code");
let repos = git_tree.list();
print(`Found ${repos.len()} repositories`);
// Print repositories
for repo in repos {
print(` - ${repo}`);
}
if repos.len() == 0 {
print("No repositories found, which is unexpected");
throw "No repositories found";
}
if repos.len() != 2 {
print("No enough repositories found, needs to be 2");
throw "No enough repositories found";
}
}
// Test GitTree find method
fn test_git_tree_find() {
print("\n=== Testing GitTree find method ===");
let git_tree = gittree_new("/tmp/code");
// Search for repositories with "code" in the name
let search_pattern = "myaccount/repo"; //we need to check if we need *, would be better not
print(`Searching for repositories matching pattern: ${search_pattern}`);
let matching = git_tree.find(search_pattern);
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
if matching.len() == 0 {
print("No matching repositories found, which is unexpected");
throw "No matching repositories found";
}
if repos.len() != 2 {
print("No enough repositories found, needs to be 2");
throw "No enough repositories found";
}
}
// Test GitRepo operations
fn test_git_repo_operations() {
print("\n=== Testing GitRepo operations ===");
let git_tree = gittree_new("/tmp/code");
let repos = git_tree.list();
if repos.len() == 0 {
print("No repositories found, which is unexpected");
throw "No repositories found";
}
// Get the first repo
let repo_path = repos[0];
print(`Testing operations on repository: ${repo_path}`);
// Get GitRepo object
let git_repos = git_tree.get(repo_path);
if git_repos.len() == 0 {
print("Failed to get GitRepo object");
throw "Failed to get GitRepo object";
}
let git_repo = git_repos[0];
// Test has_changes method
print("Testing has_changes method");
let has_changes = git_repo.has_changes();
print(`Repository has changes: ${has_changes}`);
// Create a change to test
print("Creating a change to test");
file_write("/tmp/code/test2.txt", "Another test file");
// Check if changes are detected
let has_changes_after = git_repo.has_changes();
print(`Repository has changes after modification: ${has_changes_after}`);
if !has_changes_after {
print("Changes not detected, which is unexpected");
throw "Changes not detected";
}
// Clean up the change
delete("/tmp/code/test2.txt");
}
// Run all tests
fn run_all_tests() {
print("Starting Git module tests...");
// Ensure test directory exists
ensure_test_dir();
// Run tests
test_git_tree_creation();
test_git_tree_list();
test_git_tree_find();
test_git_repo_operations();
print("\nAll tests completed successfully!");
}
// Run all tests
run_all_tests();

View File

@ -1,33 +0,0 @@
// Create a bash script to set up the test environment
let setup_script = `
rm -rf /tmp/code
mkdir -p /tmp/code
cd /tmp/code
mkdir -p myserver.com/myaccount/repogreen
mkdir -p myserver.com/myaccount/repored
cd myserver.com/myaccount/repogreen
git init
echo 'Initial test file' > test.txt
git add test.txt
git config --local user.email 'test@example.com'
git config --local user.name 'Test User'
git commit -m 'Initial commit'
cd myserver.com/myaccount/repored
git init
echo 'Initial test file' > test2.txt
git add test2.txt
git config --local user.email 'test@example.com'
git config --local user.name 'Test User'
git commit -m 'Initial commit'
//now we have 2 repos
`;
// Run the setup script
let result = run(setup_script);

42
src/test_git.rhai Normal file
View File

@ -0,0 +1,42 @@
// Simple test script for Git module functions
// Print a header
print("=== Testing Git Module Functions ===\n");
// Test git_list function
print("Listing git repositories...");
let repos = git_list();
print(`Found ${repos.len()} repositories`);
// Print the first few repositories
if repos.len() > 0 {
print("First few repositories:");
let count = if repos.len() > 3 { 3 } else { repos.len() };
for i in range(0, count) {
print(` - ${repos[i]}`);
}
}
// Test find_matching_repos function
if repos.len() > 0 {
print("\nTesting repository search...");
// Extract a part of the first repo name to search for
let repo_path = repos[0];
let parts = repo_path.split("/");
let repo_name = parts[parts.len() - 1];
print(`Searching for repositories containing "${repo_name}"`);
let matching = find_matching_repos(repo_name);
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
// Check if a repository has changes
print("\nChecking for changes in repository...");
let has_changes = git_has_changes(repo_path);
print(`Repository ${repo_path} has changes: ${has_changes}`);
}
print("\n=== Git Module Test Complete ===");

View File

@ -1,458 +0,0 @@
use crate::process::CommandResult;
use crate::virt::buildah::{execute_buildah_command, BuildahError, Image};
use std::collections::HashMap;
/// Builder struct for buildah operations
#[derive(Clone)]
pub struct Builder {
/// Name of the container
name: String,
/// Container ID
container_id: Option<String>,
/// Base image
image: String,
}
impl Builder {
/// Create a new builder with a container from the specified image
///
/// # Arguments
///
/// * `name` - Name for the container
/// * `image` - Image to create the container from
///
/// # Returns
///
/// * `Result<Self, BuildahError>` - Builder instance or error
pub fn new(name: &str, image: &str) -> Result<Self, BuildahError> {
// Try to create a new container
let result = execute_buildah_command(&["from", "--name", name, image]);
match result {
Ok(success_result) => {
// Container created successfully
let container_id = success_result.stdout.trim().to_string();
Ok(Self {
name: name.to_string(),
container_id: Some(container_id),
image: image.to_string(),
})
},
Err(BuildahError::CommandFailed(error_msg)) => {
// Check if the error is because the container already exists
if error_msg.contains("that name is already in use") {
// Extract the container ID from the error message
// Error format: "the container name "name" is already in use by container_id. You have to remove that container to be able to reuse that name: that name is already in use"
let container_id = error_msg
.split("already in use by ")
.nth(1)
.and_then(|s| s.split('.').next())
.unwrap_or("")
.trim()
.to_string();
if !container_id.is_empty() {
// Container already exists, continue with it
Ok(Self {
name: name.to_string(),
container_id: Some(container_id),
image: image.to_string(),
})
} else {
// Couldn't extract container ID
Err(BuildahError::Other("Failed to extract container ID from error message".to_string()))
}
} else {
// Other command failure
Err(BuildahError::CommandFailed(error_msg))
}
},
Err(e) => {
// Other error
Err(e)
}
}
}
/// Get the container ID
pub fn container_id(&self) -> Option<&String> {
self.container_id.as_ref()
}
/// Get the container name
pub fn name(&self) -> &str {
&self.name
}
/// Get the base image
pub fn image(&self) -> &str {
&self.image
}
/// Run a command in the container
///
/// # Arguments
///
/// * `command` - The command to run
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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()))
}
}
/// Run a command in the container with specified isolation
///
/// # Arguments
///
/// * `command` - The command to run
/// * `isolation` - Isolation method (e.g., "chroot", "rootless", "oci")
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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()))
}
}
/// Copy files into the container
///
/// # Arguments
///
/// * `source` - Source path
/// * `dest` - Destination path in the container
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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()))
}
}
/// Add files into the container
///
/// # Arguments
///
/// * `source` - Source path
/// * `dest` - Destination path in the container
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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()))
}
}
/// Commit the container to an image
///
/// # Arguments
///
/// * `image_name` - Name for the new image
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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()))
}
}
/// Remove the container
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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()))
}
}
/// Reset the builder by removing the container and clearing the container_id
///
/// # Returns
///
/// * `Result<(), BuildahError>` - Success or error
pub fn reset(&mut self) -> Result<(), BuildahError> {
if let Some(container_id) = &self.container_id {
// Try to remove the container
let result = execute_buildah_command(&["rm", container_id]);
// Clear the container_id regardless of whether the removal succeeded
self.container_id = None;
// Return the result of the removal operation
match result {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
} else {
// No container to remove
Ok(())
}
}
/// Configure container metadata
///
/// # Arguments
///
/// * `options` - Map of configuration options
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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()))
}
}
/// List images in local storage
///
/// # Returns
///
/// * `Result<Vec<Image>, BuildahError>` - List of images or error
pub fn images() -> Result<Vec<Image>, BuildahError> {
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)))
}
}
}
/// Remove an image
///
/// # Arguments
///
/// * `image` - Image ID or name
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
pub fn image_remove(image: &str) -> Result<CommandResult, BuildahError> {
execute_buildah_command(&["rmi", image])
}
/// Pull an image from a registry
///
/// # Arguments
///
/// * `image` - Image name
/// * `tls_verify` - Whether to verify TLS
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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)
}
/// Push an image to a registry
///
/// # Arguments
///
/// * `image` - Image name
/// * `destination` - Destination registry
/// * `tls_verify` - Whether to verify TLS
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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)
}
/// Tag an image
///
/// # Arguments
///
/// * `image` - Image ID or name
/// * `new_name` - New tag for the image
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, BuildahError> {
execute_buildah_command(&["tag", image, new_name])
}
/// Commit a container to an image with advanced options
///
/// # Arguments
///
/// * `container` - Container ID or name
/// * `image_name` - Name for the new image
/// * `format` - Optional format (oci or docker)
/// * `squash` - Whether to squash layers
/// * `rm` - Whether to remove the container after commit
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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)
}
/// Build an image from a Containerfile/Dockerfile
///
/// # Arguments
///
/// * `tag` - Optional tag for the image
/// * `context_dir` - Directory containing the Containerfile/Dockerfile
/// * `file` - Path to the Containerfile/Dockerfile
/// * `isolation` - Optional isolation method
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
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)
}
}

View File

@ -11,7 +11,6 @@ mod tests {
lazy_static! { lazy_static! {
static ref LAST_COMMAND: Mutex<Vec<String>> = Mutex::new(Vec::new()); static ref LAST_COMMAND: Mutex<Vec<String>> = Mutex::new(Vec::new());
static ref SHOULD_FAIL: Mutex<bool> = Mutex::new(false); static ref SHOULD_FAIL: Mutex<bool> = Mutex::new(false);
static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); // Add a mutex for test synchronization
} }
fn reset_test_state() { fn reset_test_state() {
@ -118,7 +117,6 @@ mod tests {
// Tests for each function // Tests for each function
#[test] #[test]
fn test_from_function() { fn test_from_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
let image = "alpine:latest"; let image = "alpine:latest";
@ -131,7 +129,6 @@ mod tests {
#[test] #[test]
fn test_run_function() { fn test_run_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
let container = "my-container"; let container = "my-container";
@ -146,7 +143,6 @@ mod tests {
#[test] #[test]
fn test_bah_run_with_isolation_function() { fn test_bah_run_with_isolation_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
let container = "my-container"; let container = "my-container";
@ -161,7 +157,6 @@ mod tests {
#[test] #[test]
fn test_bah_copy_function() { fn test_bah_copy_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
let container = "my-container"; let container = "my-container";
@ -176,7 +171,6 @@ mod tests {
#[test] #[test]
fn test_bah_add_function() { fn test_bah_add_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
let container = "my-container"; let container = "my-container";
@ -191,7 +185,6 @@ mod tests {
#[test] #[test]
fn test_bah_commit_function() { fn test_bah_commit_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
let container = "my-container"; let container = "my-container";
@ -205,7 +198,6 @@ mod tests {
#[test] #[test]
fn test_bah_remove_function() { fn test_bah_remove_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
let container = "my-container"; let container = "my-container";
@ -218,7 +210,6 @@ mod tests {
#[test] #[test]
fn test_bah_list_function() { fn test_bah_list_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
let result = test_bah_list(); let result = test_bah_list();
@ -230,7 +221,6 @@ mod tests {
#[test] #[test]
fn test_bah_build_function() { fn test_bah_build_function() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
// Test with tag, context directory, file, and no isolation // Test with tag, context directory, file, and no isolation
@ -239,16 +229,12 @@ mod tests {
let cmd = get_last_command(); let cmd = get_last_command();
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "-f", "Dockerfile", "."]); assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "-f", "Dockerfile", "."]);
reset_test_state(); // Reset state between sub-tests
// Test with tag, context directory, file, and isolation // Test with tag, context directory, file, and isolation
let result = test_bah_build(Some("my-app:latest"), ".", "Dockerfile.custom", Some("chroot")); let result = test_bah_build(Some("my-app:latest"), ".", "Dockerfile.custom", Some("chroot"));
assert!(result.is_ok()); assert!(result.is_ok());
let cmd = get_last_command(); let cmd = get_last_command();
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "--isolation", "chroot", "-f", "Dockerfile.custom", "."]); assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "--isolation", "chroot", "-f", "Dockerfile.custom", "."]);
reset_test_state(); // Reset state between sub-tests
// Test with just context directory and file // Test with just context directory and file
let result = test_bah_build(None, ".", "Dockerfile", None); let result = test_bah_build(None, ".", "Dockerfile", None);
assert!(result.is_ok()); assert!(result.is_ok());
@ -258,7 +244,6 @@ mod tests {
#[test] #[test]
fn test_error_handling() { fn test_error_handling() {
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
reset_test_state(); reset_test_state();
set_should_fail(true); set_should_fail(true);

View File

@ -1,7 +1,6 @@
mod containers; mod containers;
mod images; mod images;
mod cmd; mod cmd;
mod builder;
#[cfg(test)] #[cfg(test)]
mod containers_test; mod containers_test;
@ -44,12 +43,6 @@ impl Error for BuildahError {
} }
} }
} }
// Re-export the Builder
pub use builder::Builder;
// Re-export existing functions for backward compatibility
#[deprecated(since = "0.2.0", note = "Use Builder::new() instead")]
pub use containers::*; pub use containers::*;
#[deprecated(since = "0.2.0", note = "Use Builder methods instead")]
pub use images::*; pub use images::*;
pub use cmd::*; pub use cmd::*;

View File

@ -1,374 +0,0 @@
# Container API for nerdctl
This module provides a Rust API for managing containers using nerdctl, a Docker-compatible CLI for containerd.
## Overview
The Container API is designed with a builder pattern to make it easy to create and manage containers. It provides a fluent interface for configuring container options and performing operations on containers.
## Key Components
- `Container`: The main struct representing a container
- `HealthCheck`: Configuration for container health checks
- `ContainerStatus`: Information about a container's status
- `ResourceUsage`: Information about a container's resource usage
## Getting Started
### Prerequisites
- nerdctl must be installed on your system
- containerd must be running
### Basic Usage
Add the following to your `Cargo.toml`:
```toml
[dependencies]
sal = { path = "/path/to/sal" }
```
Then import the Container API in your Rust code:
```rust
use sal::virt::nerdctl::Container;
```
## Usage Examples
### Getting a Container by Name
You can get a reference to an existing container by name:
```rust
use sal::virt::nerdctl::Container;
// Get a container by name (if it exists)
match Container::new("existing-container") {
Ok(container) => {
if container.container_id.is_some() {
println!("Found container with ID: {}", container.container_id.unwrap());
// Perform operations on the existing container
let status = container.status()?;
println!("Container status: {}", status.status);
} else {
println!("Container exists but has no ID");
}
},
Err(e) => {
println!("Error getting container: {}", e);
}
}
```
### Creating a Container
You can create a new container from an image using the builder pattern:
```rust
use sal::virt::nerdctl::Container;
// Create a container from an image
let container = Container::from_image("my-nginx", "nginx:latest")?
.with_port("8080:80")
.with_env("NGINX_HOST", "example.com")
.with_volume("/tmp/nginx:/usr/share/nginx/html")
.with_health_check("curl -f http://localhost/ || exit 1")
.with_detach(true)
.build()?;
```
### Container Operations
Once you have a container, you can perform various operations on it:
```rust
// 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 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()?;
```
## Container Configuration Options
The Container API supports a wide range of configuration options through its builder pattern:
### Ports
Map container ports to host ports:
```rust
// Map a single port
.with_port("8080:80")
// Map multiple ports
.with_ports(&["8080:80", "8443:443"])
```
### Volumes
Mount host directories or volumes in the container:
```rust
// Mount a single volume
.with_volume("/host/path:/container/path")
// Mount multiple volumes
.with_volumes(&["/host/path1:/container/path1", "/host/path2:/container/path2"])
```
### Environment Variables
Set environment variables in the container:
```rust
// Set a single environment variable
.with_env("KEY", "value")
// Set multiple environment variables
let mut env_map = HashMap::new();
env_map.insert("KEY1", "value1");
env_map.insert("KEY2", "value2");
.with_envs(&env_map)
```
### Network Configuration
Configure container networking:
```rust
// Set the network
.with_network("bridge")
// Add a network alias
.with_network_alias("my-container")
// Add multiple network aliases
.with_network_aliases(&["alias1", "alias2"])
```
### Resource Limits
Set CPU and memory limits:
```rust
// Set CPU limit (e.g., 0.5 for half a CPU, 2 for 2 CPUs)
.with_cpu_limit("0.5")
// Set memory limit (e.g., 512m for 512MB, 1g for 1GB)
.with_memory_limit("512m")
// Set memory swap limit
.with_memory_swap_limit("1g")
// Set CPU shares (relative weight)
.with_cpu_shares("1024")
```
### Health Checks
Configure container health checks:
```rust
// Simple health check
.with_health_check("curl -f http://localhost/ || exit 1")
// Health check with custom options
.with_health_check_options(
"curl -f http://localhost/ || exit 1", // Command
Some("30s"), // Interval
Some("10s"), // Timeout
Some(3), // Retries
Some("5s") // Start period
)
```
### Other Options
Other container configuration options:
```rust
// Set restart policy
.with_restart_policy("always") // Options: no, always, on-failure, unless-stopped
// Set snapshotter
.with_snapshotter("native") // Options: native, fuse-overlayfs, etc.
// Set detach mode
.with_detach(true) // Run in detached mode
```
## Container Operations
Once a container is created, you can perform various operations on it:
### Basic Operations
```rust
// Start the container
container.start()?;
// Stop the container
container.stop()?;
// Remove the container
container.remove()?;
```
### Command Execution
```rust
// Execute a command in the container
let result = container.exec("echo 'Hello from container'")?;
println!("Command output: {}", result.stdout);
```
### File Operations
```rust
// Copy files between the container and host
container.copy("container_name:/path/in/container", "/path/on/host")?;
container.copy("/path/on/host", "container_name:/path/in/container")?;
// Export the container to a tarball
container.export("/path/to/export.tar")?;
```
### Image Operations
```rust
// Commit the container to an image
container.commit("my-custom-image:latest")?;
```
### Status and Monitoring
```rust
// Get container status
let status = container.status()?;
println!("Container state: {}", status.state);
println!("Container status: {}", status.status);
println!("Created: {}", status.created);
println!("Started: {}", status.started);
// Get health status
let health_status = container.health_status()?;
println!("Health status: {}", health_status);
// Get resource usage
let resources = container.resources()?;
println!("CPU usage: {}", resources.cpu_usage);
println!("Memory usage: {}", resources.memory_usage);
println!("Memory limit: {}", resources.memory_limit);
println!("Memory percentage: {}", resources.memory_percentage);
println!("Network I/O: {} / {}", resources.network_input, resources.network_output);
println!("Block I/O: {} / {}", resources.block_input, resources.block_output);
println!("PIDs: {}", resources.pids);
```
## Error Handling
The Container API uses a custom error type `NerdctlError` that can be one of the following:
- `CommandExecutionFailed`: The nerdctl command failed to execute
- `CommandFailed`: The nerdctl command executed but returned an error
- `JsonParseError`: Failed to parse JSON output
- `ConversionError`: Failed to convert data
- `Other`: Generic error
Example error handling:
```rust
match Container::new("non-existent-container") {
Ok(container) => {
// Container exists
println!("Container found");
},
Err(e) => {
match e {
NerdctlError::CommandExecutionFailed(io_error) => {
println!("Failed to execute nerdctl command: {}", io_error);
},
NerdctlError::CommandFailed(error_msg) => {
println!("nerdctl command failed: {}", error_msg);
},
_ => {
println!("Other error: {}", e);
}
}
}
}
```
## Implementation Details
The Container API is implemented in several modules:
- `container_types.rs`: Contains the struct definitions
- `container.rs`: Contains the main Container implementation
- `container_builder.rs`: Contains the builder pattern methods
- `container_operations.rs`: Contains the container operations
- `health_check.rs`: Contains the HealthCheck implementation
This modular approach makes the code more maintainable and easier to understand.
## Complete Example
Here's a complete example that demonstrates the Container API:
```rust
use std::error::Error;
use sal::virt::nerdctl::Container;
fn main() -> Result<(), Box<dyn Error>> {
// Create a container from an image
println!("Creating container from image...");
let container = Container::from_image("my-nginx", "nginx:latest")?
.with_port("8080:80")
.with_env("NGINX_HOST", "example.com")
.with_volume("/tmp/nginx:/usr/share/nginx/html")
.with_health_check("curl -f http://localhost/ || exit 1")
.with_detach(true)
.build()?;
println!("Container created successfully");
// Execute a command in the container
println!("Executing command in container...");
let result = container.exec("echo 'Hello from container'")?;
println!("Command output: {}", result.stdout);
// Get container status
println!("Getting container status...");
let status = container.status()?;
println!("Container status: {}", status.status);
// Get resource usage
println!("Getting resource usage...");
let resources = container.resources()?;
println!("CPU usage: {}", resources.cpu_usage);
println!("Memory usage: {}", resources.memory_usage);
// Stop and remove the container
println!("Stopping and removing container...");
container.stop()?;
container.remove()?;
println!("Container stopped and removed");
Ok(())
}

View File

@ -1,69 +0,0 @@
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container.rs
use std::collections::HashMap;
use crate::process::CommandResult;
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
use super::container_types::{Container, HealthCheck, ContainerStatus, ResourceUsage};
impl Container {
/// Create a new container reference with the given name
///
/// # Arguments
///
/// * `name` - Name for the container
///
/// # Returns
///
/// * `Result<Self, NerdctlError>` - Container instance or error
pub fn new(name: &str) -> Result<Self, NerdctlError> {
// Check if container exists
let result = execute_nerdctl_command(&["ps", "-a", "--format", "{{.Names}} {{.ID}}"])?;
// Look for the container name in the output
let container_id = result.stdout.lines()
.filter_map(|line| {
if line.starts_with(&format!("{} ", name)) {
Some(line.split_whitespace().nth(1)?.to_string())
} else {
None
}
})
.next();
Ok(Self {
name: name.to_string(),
container_id,
image: None,
config: HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: HashMap::new(),
network: None,
network_aliases: Vec::new(),
cpu_limit: None,
memory_limit: None,
memory_swap_limit: None,
cpu_shares: None,
restart_policy: None,
health_check: None,
detach: false,
snapshotter: None,
})
}
/// Create a container from an image
///
/// # Arguments
///
/// * `name` - Name for the container
/// * `image` - Image to create the container from
///
/// # Returns
///
/// * `Result<Self, NerdctlError>` - Container instance or error
pub fn from_image(name: &str, image: &str) -> Result<Self, NerdctlError> {
let mut container = Self::new(name)?;
container.image = Some(image.to_string());
Ok(container)
}
}

View File

@ -1,460 +0,0 @@
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_builder.rs
use std::collections::HashMap;
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
use super::container_types::{Container, HealthCheck};
impl Container {
/// Add a port mapping
///
/// # Arguments
///
/// * `port` - Port mapping (e.g., "8080:80")
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_port(mut self, port: &str) -> Self {
self.ports.push(port.to_string());
self
}
/// Add multiple port mappings
///
/// # Arguments
///
/// * `ports` - Array of port mappings (e.g., ["8080:80", "8443:443"])
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_ports(mut self, ports: &[&str]) -> Self {
for port in ports {
self.ports.push(port.to_string());
}
self
}
/// Add a volume mount
///
/// # Arguments
///
/// * `volume` - Volume mount (e.g., "/host/path:/container/path")
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_volume(mut self, volume: &str) -> Self {
self.volumes.push(volume.to_string());
self
}
/// Add multiple volume mounts
///
/// # Arguments
///
/// * `volumes` - Array of volume mounts (e.g., ["/host/path1:/container/path1", "/host/path2:/container/path2"])
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_volumes(mut self, volumes: &[&str]) -> Self {
for volume in volumes {
self.volumes.push(volume.to_string());
}
self
}
/// Add an environment variable
///
/// # Arguments
///
/// * `key` - Environment variable name
/// * `value` - Environment variable value
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_env(mut self, key: &str, value: &str) -> Self {
self.env_vars.insert(key.to_string(), value.to_string());
self
}
/// Add multiple environment variables
///
/// # Arguments
///
/// * `env_map` - Map of environment variable names to values
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_envs(mut self, env_map: &HashMap<&str, &str>) -> Self {
for (key, value) in env_map {
self.env_vars.insert(key.to_string(), value.to_string());
}
self
}
/// Set the network for the container
///
/// # Arguments
///
/// * `network` - Network name
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_network(mut self, network: &str) -> Self {
self.network = Some(network.to_string());
self
}
/// Add a network alias for the container
///
/// # Arguments
///
/// * `alias` - Network alias
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_network_alias(mut self, alias: &str) -> Self {
self.network_aliases.push(alias.to_string());
self
}
/// Add multiple network aliases for the container
///
/// # Arguments
///
/// * `aliases` - Array of network aliases
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_network_aliases(mut self, aliases: &[&str]) -> Self {
for alias in aliases {
self.network_aliases.push(alias.to_string());
}
self
}
/// Set CPU limit for the container
///
/// # Arguments
///
/// * `cpus` - CPU limit (e.g., "0.5" for half a CPU, "2" for 2 CPUs)
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_cpu_limit(mut self, cpus: &str) -> Self {
self.cpu_limit = Some(cpus.to_string());
self
}
/// Set memory limit for the container
///
/// # Arguments
///
/// * `memory` - Memory limit (e.g., "512m" for 512MB, "1g" for 1GB)
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_memory_limit(mut self, memory: &str) -> Self {
self.memory_limit = Some(memory.to_string());
self
}
/// Set memory swap limit for the container
///
/// # Arguments
///
/// * `memory_swap` - Memory swap limit (e.g., "1g" for 1GB)
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_memory_swap_limit(mut self, memory_swap: &str) -> Self {
self.memory_swap_limit = Some(memory_swap.to_string());
self
}
/// Set CPU shares for the container (relative weight)
///
/// # Arguments
///
/// * `shares` - CPU shares (e.g., "1024" for default, "512" for half)
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_cpu_shares(mut self, shares: &str) -> Self {
self.cpu_shares = Some(shares.to_string());
self
}
/// Set restart policy for the container
///
/// # Arguments
///
/// * `policy` - Restart policy (e.g., "no", "always", "on-failure", "unless-stopped")
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_restart_policy(mut self, policy: &str) -> Self {
self.restart_policy = Some(policy.to_string());
self
}
/// Set a simple health check for the container
///
/// # Arguments
///
/// * `cmd` - Command to run for health check (e.g., "curl -f http://localhost/ || exit 1")
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_health_check(mut self, cmd: &str) -> Self {
self.health_check = Some(HealthCheck {
cmd: cmd.to_string(),
interval: None,
timeout: None,
retries: None,
start_period: None,
});
self
}
/// Set a health check with custom options for the container
///
/// # Arguments
///
/// * `cmd` - Command to run for health check
/// * `interval` - Optional time between running the check (e.g., "30s", "1m")
/// * `timeout` - Optional maximum time to wait for a check to complete (e.g., "30s", "1m")
/// * `retries` - Optional number of consecutive failures needed to consider unhealthy
/// * `start_period` - Optional start period for the container to initialize before counting retries (e.g., "30s", "1m")
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_health_check_options(
mut self,
cmd: &str,
interval: Option<&str>,
timeout: Option<&str>,
retries: Option<u32>,
start_period: Option<&str>,
) -> Self {
let mut health_check = HealthCheck {
cmd: cmd.to_string(),
interval: None,
timeout: None,
retries: None,
start_period: None,
};
if let Some(interval_value) = interval {
health_check.interval = Some(interval_value.to_string());
}
if let Some(timeout_value) = timeout {
health_check.timeout = Some(timeout_value.to_string());
}
if let Some(retries_value) = retries {
health_check.retries = Some(retries_value);
}
if let Some(start_period_value) = start_period {
health_check.start_period = Some(start_period_value.to_string());
}
self.health_check = Some(health_check);
self
}
/// Set the snapshotter
///
/// # Arguments
///
/// * `snapshotter` - Snapshotter to use
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_snapshotter(mut self, snapshotter: &str) -> Self {
self.snapshotter = Some(snapshotter.to_string());
self
}
/// Set whether to run in detached mode
///
/// # Arguments
///
/// * `detach` - Whether to run in detached mode
///
/// # Returns
///
/// * `Self` - The container instance for method chaining
pub fn with_detach(mut self, detach: bool) -> Self {
self.detach = detach;
self
}
/// Build the container
///
/// # Returns
///
/// * `Result<Self, NerdctlError>` - Container instance or error
pub fn build(self) -> Result<Self, NerdctlError> {
// If container already exists, return it
if self.container_id.is_some() {
return Ok(self);
}
// If no image is specified, return an error
let image = match &self.image {
Some(img) => img,
None => return Err(NerdctlError::Other("No image specified for container creation".to_string())),
};
// Build the command arguments as strings
let mut args_strings = Vec::new();
args_strings.push("run".to_string());
if self.detach {
args_strings.push("-d".to_string());
}
args_strings.push("--name".to_string());
args_strings.push(self.name.clone());
// Add port mappings
for port in &self.ports {
args_strings.push("-p".to_string());
args_strings.push(port.clone());
}
// Add volume mounts
for volume in &self.volumes {
args_strings.push("-v".to_string());
args_strings.push(volume.clone());
}
// Add environment variables
for (key, value) in &self.env_vars {
args_strings.push("-e".to_string());
args_strings.push(format!("{}={}", key, value));
}
// Add network configuration
if let Some(network) = &self.network {
args_strings.push("--network".to_string());
args_strings.push(network.clone());
}
// Add network aliases
for alias in &self.network_aliases {
args_strings.push("--network-alias".to_string());
args_strings.push(alias.clone());
}
// Add resource limits
if let Some(cpu_limit) = &self.cpu_limit {
args_strings.push("--cpus".to_string());
args_strings.push(cpu_limit.clone());
}
if let Some(memory_limit) = &self.memory_limit {
args_strings.push("--memory".to_string());
args_strings.push(memory_limit.clone());
}
if let Some(memory_swap_limit) = &self.memory_swap_limit {
args_strings.push("--memory-swap".to_string());
args_strings.push(memory_swap_limit.clone());
}
if let Some(cpu_shares) = &self.cpu_shares {
args_strings.push("--cpu-shares".to_string());
args_strings.push(cpu_shares.clone());
}
// Add restart policy
if let Some(restart_policy) = &self.restart_policy {
args_strings.push("--restart".to_string());
args_strings.push(restart_policy.clone());
}
// Add health check
if let Some(health_check) = &self.health_check {
args_strings.push("--health-cmd".to_string());
args_strings.push(health_check.cmd.clone());
if let Some(interval) = &health_check.interval {
args_strings.push("--health-interval".to_string());
args_strings.push(interval.clone());
}
if let Some(timeout) = &health_check.timeout {
args_strings.push("--health-timeout".to_string());
args_strings.push(timeout.clone());
}
if let Some(retries) = &health_check.retries {
args_strings.push("--health-retries".to_string());
args_strings.push(retries.to_string());
}
if let Some(start_period) = &health_check.start_period {
args_strings.push("--health-start-period".to_string());
args_strings.push(start_period.clone());
}
}
if let Some(snapshotter_value) = &self.snapshotter {
args_strings.push("--snapshotter".to_string());
args_strings.push(snapshotter_value.clone());
}
// Add flags to avoid BPF issues
args_strings.push("--cgroup-manager=cgroupfs".to_string());
args_strings.push(image.clone());
// Convert to string slices for the command
let args: Vec<&str> = args_strings.iter().map(|s| s.as_str()).collect();
// Execute the command
let result = execute_nerdctl_command(&args)?;
// Get the container ID from the output
let container_id = result.stdout.trim().to_string();
Ok(Self {
name: self.name,
container_id: Some(container_id),
image: self.image,
config: self.config,
ports: self.ports,
volumes: self.volumes,
env_vars: self.env_vars,
network: self.network,
network_aliases: self.network_aliases,
cpu_limit: self.cpu_limit,
memory_limit: self.memory_limit,
memory_swap_limit: self.memory_swap_limit,
cpu_shares: self.cpu_shares,
restart_policy: self.restart_policy,
health_check: self.health_check,
detach: self.detach,
snapshotter: self.snapshotter,
})
}
}

View File

@ -1,317 +0,0 @@
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_operations.rs
use crate::process::CommandResult;
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
use super::container_types::{Container, ContainerStatus, ResourceUsage};
use serde_json;
impl Container {
/// Start the container
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn start(&self) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["start", container_id])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Stop the container
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn stop(&self) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["stop", container_id])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Remove the container
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn remove(&self) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["rm", container_id])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Execute a command in the container
///
/// # Arguments
///
/// * `command` - The command to run
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn exec(&self, command: &str) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["exec", container_id, "sh", "-c", command])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Copy files between container and local filesystem
///
/// # Arguments
///
/// * `source` - Source path (can be container:path or local path)
/// * `dest` - Destination path (can be container:path or local path)
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, NerdctlError> {
if self.container_id.is_some() {
execute_nerdctl_command(&["cp", source, dest])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Export the container to a tarball
///
/// # Arguments
///
/// * `path` - Path to save the tarball
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn export(&self, path: &str) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["export", "-o", path, container_id])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Commit the container to an image
///
/// # Arguments
///
/// * `image_name` - Name for the new image
///
/// # Returns
///
/// * `Result<CommandResult, NerdctlError>` - Command result or error
pub fn commit(&self, image_name: &str) -> Result<CommandResult, NerdctlError> {
if let Some(container_id) = &self.container_id {
execute_nerdctl_command(&["commit", container_id, image_name])
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Get container status
///
/// # Returns
///
/// * `Result<ContainerStatus, NerdctlError>` - Container status or error
pub fn status(&self) -> Result<ContainerStatus, NerdctlError> {
if let Some(container_id) = &self.container_id {
let result = execute_nerdctl_command(&["inspect", container_id])?;
// Parse the JSON output
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
Ok(json) => {
if let Some(container_json) = json.as_array().and_then(|arr| arr.first()) {
let state = container_json
.get("State")
.and_then(|state| state.get("Status"))
.and_then(|status| status.as_str())
.unwrap_or("unknown")
.to_string();
let status = container_json
.get("State")
.and_then(|state| state.get("Running"))
.and_then(|running| {
if running.as_bool().unwrap_or(false) {
Some("running")
} else {
Some("stopped")
}
})
.unwrap_or("unknown")
.to_string();
let created = container_json
.get("Created")
.and_then(|created| created.as_str())
.unwrap_or("unknown")
.to_string();
let started = container_json
.get("State")
.and_then(|state| state.get("StartedAt"))
.and_then(|started| started.as_str())
.unwrap_or("unknown")
.to_string();
// Get health status if available
let health_status = container_json
.get("State")
.and_then(|state| state.get("Health"))
.and_then(|health| health.get("Status"))
.and_then(|status| status.as_str())
.map(|s| s.to_string());
// Get health check output if available
let health_output = container_json
.get("State")
.and_then(|state| state.get("Health"))
.and_then(|health| health.get("Log"))
.and_then(|log| log.as_array())
.and_then(|log_array| log_array.last())
.and_then(|last_log| last_log.get("Output"))
.and_then(|output| output.as_str())
.map(|s| s.to_string());
Ok(ContainerStatus {
state,
status,
created,
started,
health_status,
health_output,
})
} else {
Err(NerdctlError::JsonParseError("Invalid container inspect JSON".to_string()))
}
},
Err(e) => {
Err(NerdctlError::JsonParseError(format!("Failed to parse container inspect JSON: {}", e)))
}
}
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Get the health status of the container
///
/// # Returns
///
/// * `Result<String, NerdctlError>` - Health status or error
pub fn health_status(&self) -> Result<String, NerdctlError> {
if let Some(container_id) = &self.container_id {
let result = execute_nerdctl_command(&["inspect", "--format", "{{.State.Health.Status}}", container_id])?;
Ok(result.stdout.trim().to_string())
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
/// Get container resource usage
///
/// # Returns
///
/// * `Result<ResourceUsage, NerdctlError>` - Resource usage or error
pub fn resources(&self) -> Result<ResourceUsage, NerdctlError> {
if let Some(container_id) = &self.container_id {
let result = execute_nerdctl_command(&["stats", "--no-stream", container_id])?;
// Parse the output
let lines: Vec<&str> = result.stdout.lines().collect();
if lines.len() >= 2 {
let headers = lines[0];
let values = lines[1];
let headers_vec: Vec<&str> = headers.split_whitespace().collect();
let values_vec: Vec<&str> = values.split_whitespace().collect();
// Find indices for each metric
let cpu_index = headers_vec.iter().position(|&h| h.contains("CPU")).unwrap_or(0);
let mem_index = headers_vec.iter().position(|&h| h.contains("MEM")).unwrap_or(0);
let mem_perc_index = headers_vec.iter().position(|&h| h.contains("MEM%")).unwrap_or(0);
let net_in_index = headers_vec.iter().position(|&h| h.contains("NET")).unwrap_or(0);
let net_out_index = if net_in_index > 0 { net_in_index + 1 } else { 0 };
let block_in_index = headers_vec.iter().position(|&h| h.contains("BLOCK")).unwrap_or(0);
let block_out_index = if block_in_index > 0 { block_in_index + 1 } else { 0 };
let pids_index = headers_vec.iter().position(|&h| h.contains("PIDS")).unwrap_or(0);
let cpu_usage = if cpu_index < values_vec.len() {
values_vec[cpu_index].to_string()
} else {
"unknown".to_string()
};
let memory_usage = if mem_index < values_vec.len() {
values_vec[mem_index].to_string()
} else {
"unknown".to_string()
};
let memory_limit = if mem_index + 1 < values_vec.len() {
values_vec[mem_index + 1].to_string()
} else {
"unknown".to_string()
};
let memory_percentage = if mem_perc_index < values_vec.len() {
values_vec[mem_perc_index].to_string()
} else {
"unknown".to_string()
};
let network_input = if net_in_index < values_vec.len() {
values_vec[net_in_index].to_string()
} else {
"unknown".to_string()
};
let network_output = if net_out_index < values_vec.len() {
values_vec[net_out_index].to_string()
} else {
"unknown".to_string()
};
let block_input = if block_in_index < values_vec.len() {
values_vec[block_in_index].to_string()
} else {
"unknown".to_string()
};
let block_output = if block_out_index < values_vec.len() {
values_vec[block_out_index].to_string()
} else {
"unknown".to_string()
};
let pids = if pids_index < values_vec.len() {
values_vec[pids_index].to_string()
} else {
"unknown".to_string()
};
Ok(ResourceUsage {
cpu_usage,
memory_usage,
memory_limit,
memory_percentage,
network_input,
network_output,
block_input,
block_output,
pids,
})
} else {
Err(NerdctlError::ConversionError("Failed to parse stats output".to_string()))
}
} else {
Err(NerdctlError::Other("No container ID available".to_string()))
}
}
}

View File

@ -1,76 +0,0 @@
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_test.rs
#[cfg(test)]
mod tests {
use super::super::container_types::{Container, ContainerStatus, ResourceUsage};
use super::super::NerdctlError;
use std::error::Error;
#[test]
fn test_container_builder_pattern() {
// Create a container with builder pattern
let container = Container::new("test-container").unwrap()
.with_port("8080:80")
.with_volume("/tmp:/data")
.with_env("TEST_ENV", "test_value")
.with_detach(true);
// Verify container properties
assert_eq!(container.name, "test-container");
assert_eq!(container.ports.len(), 1);
assert_eq!(container.ports[0], "8080:80");
assert_eq!(container.volumes.len(), 1);
assert_eq!(container.volumes[0], "/tmp:/data");
assert_eq!(container.env_vars.len(), 1);
assert_eq!(container.env_vars.get("TEST_ENV").unwrap(), "test_value");
assert_eq!(container.detach, true);
}
#[test]
fn test_container_from_image() {
// Create a container from image
let container = Container::from_image("test-container", "alpine:latest").unwrap();
// Verify container properties
assert_eq!(container.name, "test-container");
assert_eq!(container.image.as_ref().unwrap(), "alpine:latest");
}
#[test]
fn test_container_health_check() {
// Create a container with health check
let container = Container::new("test-container").unwrap()
.with_health_check("curl -f http://localhost/ || exit 1");
// Verify health check
assert!(container.health_check.is_some());
let health_check = container.health_check.unwrap();
assert_eq!(health_check.cmd, "curl -f http://localhost/ || exit 1");
assert!(health_check.interval.is_none());
assert!(health_check.timeout.is_none());
assert!(health_check.retries.is_none());
assert!(health_check.start_period.is_none());
}
#[test]
fn test_container_health_check_options() {
// Create a container with health check options
let container = Container::new("test-container").unwrap()
.with_health_check_options(
"curl -f http://localhost/ || exit 1",
Some("30s"),
Some("10s"),
Some(3),
Some("5s")
);
// Verify health check options
assert!(container.health_check.is_some());
let health_check = container.health_check.unwrap();
assert_eq!(health_check.cmd, "curl -f http://localhost/ || exit 1");
assert_eq!(health_check.interval.as_ref().unwrap(), "30s");
assert_eq!(health_check.timeout.as_ref().unwrap(), "10s");
assert_eq!(health_check.retries.unwrap(), 3);
assert_eq!(health_check.start_period.as_ref().unwrap(), "5s");
}
}

View File

@ -1,97 +0,0 @@
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_types.rs
use std::collections::HashMap;
/// Container struct for nerdctl operations
#[derive(Clone)]
pub struct Container {
/// Name of the container
pub name: String,
/// Container ID
pub container_id: Option<String>,
/// Base image (if created from an image)
pub image: Option<String>,
/// Configuration options
pub config: HashMap<String, String>,
/// Port mappings
pub ports: Vec<String>,
/// Volume mounts
pub volumes: Vec<String>,
/// Environment variables
pub env_vars: HashMap<String, String>,
/// Network to connect to
pub network: Option<String>,
/// Network aliases
pub network_aliases: Vec<String>,
/// CPU limit
pub cpu_limit: Option<String>,
/// Memory limit
pub memory_limit: Option<String>,
/// Memory swap limit
pub memory_swap_limit: Option<String>,
/// CPU shares
pub cpu_shares: Option<String>,
/// Restart policy
pub restart_policy: Option<String>,
/// Health check
pub health_check: Option<HealthCheck>,
/// Whether to run in detached mode
pub detach: bool,
/// Snapshotter to use
pub snapshotter: Option<String>,
}
/// Health check configuration for a container
#[derive(Debug, Clone)]
pub struct HealthCheck {
/// Command to run for health check
pub cmd: String,
/// Time between running the check (default: 30s)
pub interval: Option<String>,
/// Maximum time to wait for a check to complete (default: 30s)
pub timeout: Option<String>,
/// Number of consecutive failures needed to consider unhealthy (default: 3)
pub retries: Option<u32>,
/// Start period for the container to initialize before counting retries (default: 0s)
pub start_period: Option<String>,
}
/// Container status information
#[derive(Debug, Clone)]
pub struct ContainerStatus {
/// Container state (e.g., running, stopped)
pub state: String,
/// Container status
pub status: String,
/// Creation time
pub created: String,
/// Start time
pub started: String,
/// Health status (if health check is configured)
pub health_status: Option<String>,
/// Health check output (if health check is configured)
pub health_output: Option<String>,
}
/// Container resource usage information
#[derive(Debug, Clone)]
pub struct ResourceUsage {
/// CPU usage percentage
pub cpu_usage: String,
/// Memory usage
pub memory_usage: String,
/// Memory limit
pub memory_limit: String,
/// Memory usage percentage
pub memory_percentage: String,
/// Network input
pub network_input: String,
/// Network output
pub network_output: String,
/// Block input
pub block_input: String,
/// Block output
pub block_output: String,
/// PIDs
pub pids: String,
}

View File

@ -0,0 +1,105 @@
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/containers.rs
use crate::virt::nerdctl::execute_nerdctl_command;
use crate::process::CommandResult;
use super::NerdctlError;
/// Run a container from an image
///
/// # Arguments
///
/// * `image` - The image to run
/// * `name` - Optional container name
/// * `detach` - Whether to run in detached mode
/// * `ports` - Optional port mappings (e.g., ["8080:80"])
/// * `snapshotter` - Optional snapshotter to use (e.g., "native", "fuse-overlayfs")
pub fn run(image: &str, name: Option<&str>, detach: bool, ports: Option<&[&str]>, snapshotter: Option<&str>) -> Result<CommandResult, NerdctlError> {
// First, try to remove any existing container with the same name to avoid conflicts
if let Some(name_str) = name {
// Ignore errors since the container might not exist
let _ = execute_nerdctl_command(&["rm", "-f", name_str]);
}
let mut args = vec!["run"];
if detach {
args.push("-d");
}
if let Some(name_str) = name {
args.push("--name");
args.push(name_str);
}
if let Some(port_mappings) = ports {
for port in port_mappings {
args.push("-p");
args.push(port);
}
}
if let Some(snapshotter_value) = snapshotter {
args.push("--snapshotter");
args.push(snapshotter_value);
}
// Add flags to avoid BPF issues
args.push("--cgroup-manager=cgroupfs");
args.push(image);
execute_nerdctl_command(&args)
}
/// Execute a command in a container
///
/// # Arguments
///
/// * `container` - The container ID or name
/// * [command](cci:1://file:///root/code/git.ourworld.tf/herocode/sal/src/process/run.rs:303:0-324:1) - The command to run
pub fn exec(container: &str, command: &str) -> Result<CommandResult, NerdctlError> {
execute_nerdctl_command(&["exec", container, "sh", "-c", command])
}
/// Copy files between container and local filesystem
///
/// # Arguments
///
/// * [source](cci:1://file:///root/code/git.ourworld.tf/herocode/sal/src/process/run.rs:55:4-64:5) - Source path (can be container:path or local path)
/// * `dest` - Destination path (can be container:path or local path)
pub fn copy(source: &str, dest: &str) -> Result<CommandResult, NerdctlError> {
execute_nerdctl_command(&["cp", source, dest])
}
/// Stop a container
///
/// # Arguments
///
/// * `container` - The container ID or name
pub fn stop(container: &str) -> Result<CommandResult, NerdctlError> {
execute_nerdctl_command(&["stop", container])
}
/// Remove a container
///
/// # Arguments
///
/// * `container` - The container ID or name
pub fn remove(container: &str) -> Result<CommandResult, NerdctlError> {
execute_nerdctl_command(&["rm", container])
}
/// List containers
///
/// # Arguments
///
/// * `all` - Whether to show all containers (including stopped ones)
pub fn list(all: bool) -> Result<CommandResult, NerdctlError> {
let mut args = vec!["ps"];
if all {
args.push("-a");
}
execute_nerdctl_command(&args)
}

View File

@ -1,40 +0,0 @@
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/health_check.rs
use super::container_types::HealthCheck;
impl HealthCheck {
/// Create a new health check with the given command
pub fn new(cmd: &str) -> Self {
Self {
cmd: cmd.to_string(),
interval: None,
timeout: None,
retries: None,
start_period: None,
}
}
/// Set the interval between health checks
pub fn with_interval(mut self, interval: &str) -> Self {
self.interval = Some(interval.to_string());
self
}
/// Set the timeout for health checks
pub fn with_timeout(mut self, timeout: &str) -> Self {
self.timeout = Some(timeout.to_string());
self
}
/// Set the number of retries for health checks
pub fn with_retries(mut self, retries: u32) -> Self {
self.retries = Some(retries);
self
}
/// Set the start period for health checks
pub fn with_start_period(mut self, start_period: &str) -> Self {
self.start_period = Some(start_period.to_string());
self
}
}

View File

@ -1,12 +1,6 @@
mod containers;
mod images; mod images;
mod cmd; mod cmd;
mod container_types;
mod container;
mod container_builder;
mod health_check;
mod container_operations;
#[cfg(test)]
mod container_test;
use std::fmt; use std::fmt;
use std::error::Error; use std::error::Error;
@ -48,6 +42,6 @@ impl Error for NerdctlError {
} }
} }
pub use containers::*;
pub use images::*; pub use images::*;
pub use cmd::*; pub use cmd::*;
pub use container_types::{Container, HealthCheck, ContainerStatus, ResourceUsage};