This commit is contained in:
despiegk 2025-04-05 05:46:30 +02:00
parent e48063a79c
commit 7cdd9f5559
52 changed files with 2062 additions and 1256 deletions

View File

@ -0,0 +1,238 @@
# Container Builder Implementation Plan
## Overview
This document outlines the plan for redesigning the nerdctl interface in the `src/virt/nerdctl` directory to use an object-oriented approach with a Container struct that supports method chaining for the builder pattern. This will replace the existing function-based approach while maintaining all current functionality.
## Architecture
```mermaid
classDiagram
class Container {
-String name
-String container_id
-String? image
-HashMap~String, String~ config
-Vec~String~ ports
-Vec~String~ volumes
-HashMap~String, String~ env_vars
-Option~String~ network
-Vec~String~ network_aliases
-Option~String~ cpu_limit
-Option~String~ memory_limit
-Option~String~ memory_swap_limit
-Option~String~ cpu_shares
-Option~String~ restart_policy
-Option~HealthCheck~ health_check
+new(name: &str) -> Result~Container, NerdctlError~
+from_image(name: &str, image: &str) -> Result~Container, NerdctlError~
+with_port(port: &str) -> Container
+with_ports(ports: &[&str]) -> Container
+with_volume(volume: &str) -> Container
+with_volumes(volumes: &[&str]) -> Container
+with_env(key: &str, value: &str) -> Container
+with_envs(env_map: &HashMap<&str, &str>) -> Container
+with_network(network: &str) -> Container
+with_network_alias(alias: &str) -> Container
+with_network_aliases(aliases: &[&str]) -> Container
+with_cpu_limit(cpus: &str) -> Container
+with_memory_limit(memory: &str) -> Container
+with_memory_swap_limit(memory_swap: &str) -> Container
+with_cpu_shares(shares: &str) -> Container
+with_restart_policy(policy: &str) -> Container
+with_health_check(cmd: &str) -> Container
+with_health_check_options(cmd: &str, interval: Option<&str>, timeout: Option<&str>, retries: Option<u32>, start_period: Option<&str>) -> Container
+with_snapshotter(snapshotter: &str) -> Container
+with_detach(detach: bool) -> Container
+build() -> Result~Container, NerdctlError~
+start() -> Result~CommandResult, NerdctlError~
+stop() -> Result~CommandResult, NerdctlError~
+remove() -> Result~CommandResult, NerdctlError~
+exec(command: &str) -> Result~CommandResult, NerdctlError~
+copy(source: &str, dest: &str) -> Result~CommandResult, NerdctlError~
+export(path: &str) -> Result~CommandResult, NerdctlError~
+commit(image_name: &str) -> Result~CommandResult, NerdctlError~
+status() -> Result~ContainerStatus, NerdctlError~
+health_status() -> Result~String, NerdctlError~
+resources() -> Result~ResourceUsage, NerdctlError~
}
class HealthCheck {
+String cmd
+Option~String~ interval
+Option~String~ timeout
+Option~u32~ retries
+Option~String~ start_period
}
class ContainerStatus {
+String state
+String status
+String created
+String started
+Option~String~ health_status
+Option~String~ health_output
}
class ResourceUsage {
+String cpu_usage
+String memory_usage
+String memory_limit
+String memory_percentage
+String network_input
+String network_output
+String block_input
+String block_output
+String pids
}
class NerdctlError {
+CommandExecutionFailed(io::Error)
+CommandFailed(String)
+JsonParseError(String)
+ConversionError(String)
+Other(String)
}
Container --> ContainerStatus : returns
Container --> ResourceUsage : returns
Container --> HealthCheck : contains
Container --> NerdctlError : may throw
```
## Implementation Steps
### 1. Create Container Struct and Basic Methods
Create a new file `src/virt/nerdctl/container.rs` with the Container struct and basic methods.
### 2. Implement Builder Pattern Methods
Add builder pattern methods to the Container struct for configuration.
### 3. Implement Container Operations
Add methods for container operations like start, stop, exec, etc.
### 4. Implement Status and Resource Usage Methods
Add methods for getting container status and resource usage information.
### 5. Update mod.rs to Export the New Container Struct
Update `src/virt/nerdctl/mod.rs` to include the new container module.
### 6. Create Example Usage
Create an example file to demonstrate the new Container API.
## Key Features
### Container Creation and Configuration
- Method chaining for the builder pattern
- Support for multiple ports and volumes
- Environment variable configuration
- Network configuration and aliases
- Resource limits (CPU, memory)
- Restart policies
- Health checks
### Container Operations
- Start, stop, and remove containers
- Execute commands in containers
- Copy files between container and host
- Export containers to tarballs
- Commit containers to images
### Container Monitoring
- Get container status information
- Get container health status
- Get resource usage information
## Example Usage
```rust
// Create a container with various configuration options
let container = Container::from_image("my-web-app", "nginx:latest")?
.with_ports(&["8080:80", "8443:443"])
.with_volumes(&[
"./html:/usr/share/nginx/html",
"./config/nginx.conf:/etc/nginx/nginx.conf"
])
.with_env("NGINX_HOST", "example.com")
.with_env("NGINX_PORT", "80")
.with_network("app-network")
.with_network_alias("web")
.with_cpu_limit("0.5")
.with_memory_limit("512m")
.with_restart_policy("always")
.with_health_check_options(
"curl -f http://localhost/ || exit 1",
Some("10s"),
Some("5s"),
Some(3),
Some("30s")
)
.with_detach(true)
.build()?;
// Start the container
container.start()?;
// Execute a command in the container
let result = container.exec("echo 'Hello from container'")?;
println!("Command output: {}", result.stdout);
// Get container status
let status = container.status()?;
println!("Container state: {}", status.state);
println!("Container status: {}", status.status);
// Get resource usage
let resources = container.resources()?;
println!("CPU usage: {}", resources.cpu_usage);
println!("Memory usage: {}", resources.memory_usage);
// Stop and remove the container
container.stop()?;
container.remove()?;
```
## Network Management
```rust
// Create a network
Container::create_network("app-network", Some("bridge"))?;
// Create containers in the network
let db = Container::from_image("db", "postgres:latest")?
.with_network("app-network")
.with_network_alias("database")
.with_env("POSTGRES_PASSWORD", "example")
.build()?;
let app = Container::from_image("app", "my-app:latest")?
.with_network("app-network")
.with_env("DATABASE_URL", "postgres://postgres:example@database:5432/postgres")
.build()?;
// Remove the network when done
Container::remove_network("app-network")?;
```
## Migration Strategy
1. Create the new Container struct and its methods
2. Update the mod.rs file to export the new Container struct
3. Create example usage to demonstrate the new API
4. Deprecate the old function-based API (but keep it for backward compatibility)
5. Update documentation to reflect the new API
## Testing Strategy
1. Unit tests for the Container struct and its methods
2. Integration tests for the Container API
3. Manual testing with real containers

View File

@ -1,122 +0,0 @@
//! 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::{BuildahError, Builder};
use std::collections::HashMap;
/// Run a complete buildah workflow example
pub fn run_buildah_example() -> Result<(), BuildahError> {
println!("Starting buildah example workflow...");
// Step 1: Create a container from an image using the Builder
println!("\n=== Creating container from fedora:latest ===");
let mut builder = Builder::new("my-fedora-container", "fedora:latest")?;
// Reset the builder to remove any existing container
println!("\n=== Resetting the builder to start fresh ===");
builder.reset()?;
// Create a new container (or continue with existing one)
println!("\n=== Creating container from fedora:latest ===");
builder = Builder::new("my-fedora-container", "fedora:latest")?;
println!("Created container: {}", builder.container_id().unwrap_or(&"unknown".to_string()));
// Step 2: Run a command in the container
println!("\n=== Installing nginx in container ===");
// Use chroot isolation to avoid BPF issues
let install_result = 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().unwrap();
}

View File

@ -0,0 +1,62 @@
// 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(())
}

View File

@ -1,84 +0,0 @@
//! 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

@ -1,74 +0,0 @@
//! 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

@ -1,66 +0,0 @@
//! 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

@ -1,54 +0,0 @@
//! 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

@ -1,135 +0,0 @@
// 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"

View File

@ -1,124 +0,0 @@
// 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

@ -177,6 +177,7 @@ print("All repositories:");
for repo_path in all_repos { for repo_path in all_repos {
print(` - ${repo_path}`); print(` - ${repo_path}`);
} }
```
## Error Handling ## Error Handling

View File

@ -1,89 +0,0 @@
//! 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");
// Create a new GitTree object
let home_dir = env("HOME");
let git_tree = gittree_new(`${home_dir}/code`);
print(`Created GitTree with base path: ${home_dir}/code`);
// Test list method
print("\nListing git repositories...");
let repos = git_tree.list();
print(`Found ${repos.len()} repositories`);
// Print the first few repositories
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 method
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 = git_tree.find(repo_name + "*");
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
// Test get method
print("\nTesting get method...");
let git_repos = git_tree.get(repo_name);
print(`Found ${git_repos.len()} GitRepo objects`);
// Test GitRepo methods
if git_repos.len() > 0 {
let git_repo = git_repos[0];
print(`\nTesting GitRepo methods on: ${git_repo.path()}`);
// Check if a repository has changes
print("Checking for changes in repository...");
let has_changes = git_repo.has_changes();
print(`Repository has changes: ${has_changes}`);
// Test method chaining (only if there are no changes to avoid errors)
if !has_changes {
print("\nTesting method chaining (pull)...");
let result = git_repo.pull();
print("Pull operation completed successfully");
}
}
}
print("\n=== Git Module Test Complete ===");
"#;
// Evaluate the script
match engine.eval::<()>(script) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Script execution error: {}", e),
}
Ok(())
}

View File

@ -1,89 +0,0 @@
//! 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");
// Create a new GitTree object
let home_dir = env("HOME");
let git_tree = gittree_new(`${home_dir}/code`);
print(`Created GitTree with base path: ${home_dir}/code`);
// Test list method
print("\nListing git repositories...");
let repos = git_tree.list();
print(`Found ${repos.len()} repositories`);
// Print the first few repositories
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 method
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 = git_tree.find(repo_name + "*");
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
// Test get method
print("\nTesting get method...");
let git_repos = git_tree.get(repo_name);
print(`Found ${git_repos.len()} GitRepo objects`);
// Test GitRepo methods
if git_repos.len() > 0 {
let git_repo = git_repos[0];
print(`\nTesting GitRepo methods on: ${git_repo.path()}`);
// Check if a repository has changes
print("Checking for changes in repository...");
let has_changes = git_repo.has_changes();
print(`Repository has changes: ${has_changes}`);
// Test method chaining (only if there are no changes to avoid errors)
if !has_changes {
print("\nTesting method chaining (pull)...");
let result = git_repo.pull();
print("Pull operation completed successfully");
}
}
}
print("\n=== Git Module Test Complete ===");
"#;
// Evaluate the script
match engine.eval::<()>(script) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Script execution error: {}", e),
}
Ok(())
}

View File

@ -1,27 +0,0 @@
//! Example of running the test_git.rhai script
//!
//! This example demonstrates how to run the test_git.rhai script
//! through the Rhai scripting engine.
use sal::rhai::{self, Engine};
use std::fs;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// Create a new Rhai engine
let mut engine = Engine::new();
// Register SAL functions with the engine
rhai::register(&mut engine)?;
// Read the test script
let script = fs::read_to_string("src/test_git.rhai")?;
// Evaluate the script
match engine.eval::<()>(&script) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Script execution error: {}", e),
}
Ok(())
}

View File

@ -1,27 +0,0 @@
//! Simple example of using the Git module with Rhai
//!
//! This example demonstrates how to use the Git module functions
//! through the Rhai scripting language.
use sal::rhai::{self, Engine};
use std::fs;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// Create a new Rhai engine
let mut engine = Engine::new();
// Register SAL functions with the engine
rhai::register(&mut engine)?;
// Read the test script
let script = fs::read_to_string("simple_git_test.rhai")?;
// Evaluate the script
match engine.eval::<()>(&script) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Script execution error: {}", e),
}
Ok(())
}

View File

@ -98,16 +98,35 @@ 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/sh".to_string()); let (ext, interpreter) = (".sh", "/bin/bash".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)?;
// Write the script content // For Unix systems, ensure the script has a shebang line with -e flag
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
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()) file.write_all(dedented.as_bytes())
.map_err(RunError::FileWriteFailed)?; .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"))]
@ -166,7 +185,8 @@ 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 {
eprintln!("{}", l); // Always print stderr, even if silent is true, for error visibility
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
@ -198,6 +218,14 @@ 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,
@ -214,6 +242,20 @@ 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,
@ -260,7 +302,18 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
.args(&command_args) .args(&command_args)
.output(); .output();
process_command_output(output) let result = 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)
@ -270,16 +323,45 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
.spawn() .spawn()
.map_err(RunError::CommandExecutionFailed)?; .map_err(RunError::CommandExecutionFailed)?;
handle_child_output(child, false) let result = 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
@ -338,21 +420,41 @@ impl<'a> RunBuilder<'a> {
// Log command execution if enabled // Log command execution if enabled
if self.log { if self.log {
println!("[LOG] Executing command: {}", trimmed); println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", 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 || {
let _ = if cmd_copy.contains('\n') { if log {
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
@ -375,8 +477,17 @@ 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

@ -1,170 +0,0 @@
// Test script for Git module functions
// Import required modules
import "os" as os;
import "process" as process;
// Test GitTree creation
fn test_git_tree_creation() {
// Get home directory
let home_dir = env("HOME");
let code_dir = `${home_dir}/code`;
print("Testing GitTree creation...");
let git_tree = gittree_new(code_dir);
print(`Created GitTree with base path: ${code_dir}`);
return true;
}
// Test GitTree list method
fn test_git_tree_list() {
// Get home directory
let home_dir = env("HOME");
let code_dir = `${home_dir}/code`;
print("Testing GitTree list method...");
let git_tree = gittree_new(code_dir);
let repos = git_tree.list();
print(`Found ${repos.len()} repositories`);
// 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; // Success even if no repos found
}
// Test GitTree find method
fn test_git_tree_find() {
// Get home directory
let home_dir = env("HOME");
let code_dir = `${home_dir}/code`;
print("Testing GitTree find method...");
let git_tree = gittree_new(code_dir);
let repos = git_tree.list();
if repos.len() == 0 {
print("No repositories found, skipping test");
return true;
}
// Extract a part of the first repo name to search for
let repo_path = repos[0];
let parts = repo_path.split("/");
let repo_name = parts[parts.len() - 1];
let search_pattern = repo_name + "*";
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}`);
}
return matching.len() > 0;
}
// Test GitTree get method
fn test_git_tree_get() {
// Get home directory
let home_dir = env("HOME");
let code_dir = `${home_dir}/code`;
print("Testing GitTree get method...");
let git_tree = gittree_new(code_dir);
let repos = git_tree.list();
if repos.len() == 0 {
print("No repositories found, skipping test");
return true;
}
// Extract a part of the first repo name to search for
let repo_path = repos[0];
let parts = repo_path.split("/");
let repo_name = parts[parts.len() - 1];
print(`Getting GitRepo objects for: ${repo_name}`);
let git_repos = git_tree.get(repo_name);
print(`Found ${git_repos.len()} GitRepo objects`);
for repo in git_repos {
print(` - ${repo.path()}`);
}
return git_repos.len() > 0;
}
// Test GitRepo has_changes method
fn test_git_repo_has_changes() {
// Get home directory
let home_dir = env("HOME");
let code_dir = `${home_dir}/code`;
print("Testing GitRepo has_changes method...");
let git_tree = gittree_new(code_dir);
let repos = git_tree.list();
if repos.len() == 0 {
print("No repositories found, skipping test");
return true;
}
// Get the first repo
let git_repos = git_tree.get(repos[0]);
if git_repos.len() == 0 {
print("Failed to get GitRepo object, skipping test");
return true;
}
let git_repo = git_repos[0];
let has_changes = git_repo.has_changes();
print(`Repository ${git_repo.path()} has changes: ${has_changes}`);
return true;
}
// Run the tests
fn run_tests() {
let tests = [
#{ name: "git_tree_creation", fn: test_git_tree_creation },
#{ name: "git_tree_list", fn: test_git_tree_list },
#{ name: "git_tree_find", fn: test_git_tree_find },
#{ name: "git_tree_get", fn: test_git_tree_get },
#{ name: "git_repo_has_changes", fn: test_git_repo_has_changes }
];
let passed = 0;
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

@ -0,0 +1,164 @@
// 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

@ -0,0 +1,33 @@
// 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);

View File

@ -1,11 +0,0 @@
// Simple test script for Git module functions
// Print a header
print("=== Testing Git Module Functions ===\n");
// Create a new GitTree object
let home_dir = env("HOME");
let git_tree = gittree_new(`${home_dir}/code`);
print(`Created GitTree with base path: ${home_dir}/code`);
print("\n=== Git Module Test Complete ===");

View File

@ -1,65 +0,0 @@
// Simple test script for Git module functions
// Print a header
print("=== Testing Git Module Functions ===\n");
// Create a new GitTree object
let home_dir = env("HOME");
let git_tree = gittree_new(`${home_dir}/code`);
print(`Created GitTree with base path: ${home_dir}/code`);
// Test list method
print("\nListing git repositories...");
let repos = git_tree.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 method
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 = git_tree.find(repo_name);
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
// Test get method
print("\nTesting get method...");
let git_repos = git_tree.get(repo_name);
print(`Found ${git_repos.len()} GitRepo objects`);
// Test GitRepo methods
if git_repos.len() > 0 {
let git_repo = git_repos[0];
print(`\nTesting GitRepo methods on: ${git_repo.path()}`);
// Check if a repository has changes
print("Checking for changes in repository...");
let has_changes = git_repo.has_changes();
print(`Repository has changes: ${has_changes}`);
// Test method chaining (only if there are no changes to avoid errors)
if !has_changes {
print("\nTesting method chaining (pull)...");
let result = git_repo.pull();
print("Pull operation completed successfully");
}
}
}
print("\n=== Git Module Test Complete ===");

374
src/virt/nerdctl/README.md Normal file
View File

@ -0,0 +1,374 @@
# 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

@ -0,0 +1,69 @@
// 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

@ -0,0 +1,460 @@
// 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

View File

@ -0,0 +1,317 @@
// 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

@ -0,0 +1,76 @@
// 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

@ -0,0 +1,97 @@
// 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

@ -1,105 +0,0 @@
// 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

@ -0,0 +1,40 @@
// 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,6 +1,12 @@
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;
@ -42,6 +48,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};