Compare commits
No commits in common. "7cdd9f555934552a63c0ba76cf72a549aca7661e" and "9f33c940209ce5ac8bc27c7d266f21269230bf15" have entirely different histories.
7cdd9f5559
...
9f33c94020
@ -1,752 +0,0 @@
|
|||||||
# Buildah Builder Implementation Plan
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
This document outlines the plan for changing the buildah interface in the `@src/virt/buildah` module to use a builder object pattern. The current implementation uses standalone functions, which makes the interface less clear and harder to use. The new implementation will use a builder object with methods, which will make the interface more intuitive and easier to use.
|
|
||||||
|
|
||||||
## Current Architecture
|
|
||||||
|
|
||||||
The current buildah implementation has:
|
|
||||||
- Standalone functions in the buildah module (from, run, images, etc.)
|
|
||||||
- Functions that operate on container IDs passed as parameters
|
|
||||||
- No state maintained between function calls
|
|
||||||
- Rhai wrappers that expose these functions to Rhai scripts
|
|
||||||
|
|
||||||
Example of current usage:
|
|
||||||
```rust
|
|
||||||
// Create a container
|
|
||||||
let result = buildah::from("fedora:latest")?;
|
|
||||||
let container_id = result.stdout.trim();
|
|
||||||
|
|
||||||
// Run a command in the container
|
|
||||||
buildah::run(container_id, "dnf install -y nginx")?;
|
|
||||||
|
|
||||||
// Copy a file into the container
|
|
||||||
buildah::bah_copy(container_id, "./example.conf", "/etc/example.conf")?;
|
|
||||||
|
|
||||||
// Commit the container to create a new image
|
|
||||||
buildah::bah_commit(container_id, "my-nginx:latest")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Proposed Architecture
|
|
||||||
|
|
||||||
We'll change to a builder object pattern where:
|
|
||||||
- A `Builder` struct is created with a `new()` method that takes a name and image
|
|
||||||
- All operations (including those not specific to a container) are methods on the Builder
|
|
||||||
- The Builder maintains state (like container ID) between method calls
|
|
||||||
- Methods return operation results (CommandResult or other types) for error handling
|
|
||||||
- Rhai wrappers expose the Builder and its methods to Rhai scripts
|
|
||||||
|
|
||||||
Example of proposed usage:
|
|
||||||
```rust
|
|
||||||
// Create a builder
|
|
||||||
let builder = Builder::new("my-container", "fedora:latest")?;
|
|
||||||
|
|
||||||
// Run a command in the container
|
|
||||||
builder.run("dnf install -y nginx")?;
|
|
||||||
|
|
||||||
// Copy a file into the container
|
|
||||||
builder.copy("./example.conf", "/etc/example.conf")?;
|
|
||||||
|
|
||||||
// Commit the container to create a new image
|
|
||||||
builder.commit("my-nginx:latest")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Class Diagram
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
class Builder {
|
|
||||||
-String name
|
|
||||||
-String container_id
|
|
||||||
-String image
|
|
||||||
+new(name: String, image: String) -> Result<Builder, BuildahError>
|
|
||||||
+run(command: String) -> Result<CommandResult, BuildahError>
|
|
||||||
+run_with_isolation(command: String, isolation: String) -> Result<CommandResult, BuildahError>
|
|
||||||
+add(source: String, dest: String) -> Result<CommandResult, BuildahError>
|
|
||||||
+copy(source: String, dest: String) -> Result<CommandResult, BuildahError>
|
|
||||||
+commit(image_name: String) -> Result<CommandResult, BuildahError>
|
|
||||||
+remove() -> Result<CommandResult, BuildahError>
|
|
||||||
+config(options: HashMap<String, String>) -> Result<CommandResult, BuildahError>
|
|
||||||
}
|
|
||||||
|
|
||||||
class BuilderStatic {
|
|
||||||
+images() -> Result<Vec<Image>, BuildahError>
|
|
||||||
+image_remove(image: String) -> Result<CommandResult, BuildahError>
|
|
||||||
+image_pull(image: String, tls_verify: bool) -> Result<CommandResult, BuildahError>
|
|
||||||
+image_push(image: String, destination: String, tls_verify: bool) -> Result<CommandResult, BuildahError>
|
|
||||||
+image_tag(image: String, new_name: String) -> Result<CommandResult, BuildahError>
|
|
||||||
+image_commit(container: String, image_name: String, format: Option<String>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError>
|
|
||||||
+build(tag: Option<String>, context_dir: String, file: String, isolation: Option<String>) -> Result<CommandResult, BuildahError>
|
|
||||||
}
|
|
||||||
|
|
||||||
Builder --|> BuilderStatic : Static methods
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Step 1: Create the Builder Struct
|
|
||||||
|
|
||||||
1. Create a new file `src/virt/buildah/builder.rs`
|
|
||||||
2. Define the Builder struct with fields for name, container_id, and image
|
|
||||||
3. Implement the new() method that creates a container from the image and returns a Builder instance
|
|
||||||
4. Implement methods for all container operations (run, add, copy, commit, etc.)
|
|
||||||
5. Implement methods for all image operations (images, image_remove, image_pull, etc.)
|
|
||||||
|
|
||||||
### Step 2: Update the Module Structure
|
|
||||||
|
|
||||||
1. Update `src/virt/buildah/mod.rs` to include the new builder module
|
|
||||||
2. Re-export the Builder struct and its methods
|
|
||||||
3. Keep the existing functions for backward compatibility (marked as deprecated)
|
|
||||||
|
|
||||||
### Step 3: Update the Rhai Wrapper
|
|
||||||
|
|
||||||
1. Update `src/rhai/buildah.rs` to register the Builder type with the Rhai engine
|
|
||||||
2. Register all Builder methods with the Rhai engine
|
|
||||||
3. Create Rhai-friendly constructor for the Builder
|
|
||||||
4. Update the existing function wrappers to use the new Builder (or keep them for backward compatibility)
|
|
||||||
|
|
||||||
### Step 4: Update Examples and Tests
|
|
||||||
|
|
||||||
1. Update `examples/buildah.rs` to use the new Builder pattern
|
|
||||||
2. Update `rhaiexamples/04_buildah_operations.rhai` to use the new Builder pattern
|
|
||||||
3. Update any tests to use the new Builder pattern
|
|
||||||
|
|
||||||
## Detailed Implementation
|
|
||||||
|
|
||||||
### 1. Builder Struct Definition
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// src/virt/buildah/builder.rs
|
|
||||||
pub struct Builder {
|
|
||||||
name: String,
|
|
||||||
container_id: Option<String>,
|
|
||||||
image: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Builder {
|
|
||||||
pub fn new(name: &str, image: &str) -> Result<Self, BuildahError> {
|
|
||||||
let result = execute_buildah_command(&["from", "--name", name, image])?;
|
|
||||||
let container_id = result.stdout.trim().to_string();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
container_id: Some(container_id),
|
|
||||||
image: image.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container methods
|
|
||||||
pub fn run(&self, command: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["run", container_id, "sh", "-c", command])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_with_isolation(&self, command: &str, isolation: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["run", "--isolation", isolation, container_id, "sh", "-c", command])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["copy", container_id, source, dest])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["add", container_id, source, dest])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn commit(&self, image_name: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["commit", container_id, image_name])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&self) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["rm", container_id])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config(&self, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
let mut args_owned: Vec<String> = Vec::new();
|
|
||||||
args_owned.push("config".to_string());
|
|
||||||
|
|
||||||
// Process options map
|
|
||||||
for (key, value) in options.iter() {
|
|
||||||
let option_name = format!("--{}", key);
|
|
||||||
args_owned.push(option_name);
|
|
||||||
args_owned.push(value.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
args_owned.push(container_id.clone());
|
|
||||||
|
|
||||||
// Convert Vec<String> to Vec<&str> for execute_buildah_command
|
|
||||||
let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static methods
|
|
||||||
pub fn images() -> Result<Vec<Image>, BuildahError> {
|
|
||||||
// Implementation from current images() function
|
|
||||||
let result = execute_buildah_command(&["images", "--json"])?;
|
|
||||||
|
|
||||||
// Try to parse the JSON output
|
|
||||||
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
|
|
||||||
Ok(json) => {
|
|
||||||
if let serde_json::Value::Array(images_json) = json {
|
|
||||||
let mut images = Vec::new();
|
|
||||||
|
|
||||||
for image_json in images_json {
|
|
||||||
// Extract image ID
|
|
||||||
let id = match image_json.get("id").and_then(|v| v.as_str()) {
|
|
||||||
Some(id) => id.to_string(),
|
|
||||||
None => return Err(BuildahError::ConversionError("Missing image ID".to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract image names
|
|
||||||
let names = match image_json.get("names").and_then(|v| v.as_array()) {
|
|
||||||
Some(names_array) => {
|
|
||||||
let mut names_vec = Vec::new();
|
|
||||||
for name_value in names_array {
|
|
||||||
if let Some(name_str) = name_value.as_str() {
|
|
||||||
names_vec.push(name_str.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
names_vec
|
|
||||||
},
|
|
||||||
None => Vec::new(), // Empty vector if no names found
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract image size
|
|
||||||
let size = match image_json.get("size").and_then(|v| v.as_str()) {
|
|
||||||
Some(size) => size.to_string(),
|
|
||||||
None => "Unknown".to_string(), // Default value if size not found
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract creation timestamp
|
|
||||||
let created = match image_json.get("created").and_then(|v| v.as_str()) {
|
|
||||||
Some(created) => created.to_string(),
|
|
||||||
None => "Unknown".to_string(), // Default value if created not found
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create Image struct and add to vector
|
|
||||||
images.push(Image {
|
|
||||||
id,
|
|
||||||
names,
|
|
||||||
size,
|
|
||||||
created,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(images)
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::JsonParseError("Expected JSON array".to_string()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
Err(BuildahError::JsonParseError(format!("Failed to parse image list JSON: {}", e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn image_remove(image: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
execute_buildah_command(&["rmi", image])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
|
|
||||||
let mut args = vec!["pull"];
|
|
||||||
|
|
||||||
if !tls_verify {
|
|
||||||
args.push("--tls-verify=false");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(image);
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
|
|
||||||
let mut args = vec!["push"];
|
|
||||||
|
|
||||||
if !tls_verify {
|
|
||||||
args.push("--tls-verify=false");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(image);
|
|
||||||
args.push(destination);
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
execute_buildah_command(&["tag", image, new_name])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn image_commit(container: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError> {
|
|
||||||
let mut args = vec!["commit"];
|
|
||||||
|
|
||||||
if let Some(format_str) = format {
|
|
||||||
args.push("--format");
|
|
||||||
args.push(format_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
if squash {
|
|
||||||
args.push("--squash");
|
|
||||||
}
|
|
||||||
|
|
||||||
if rm {
|
|
||||||
args.push("--rm");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(container);
|
|
||||||
args.push(image_name);
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError> {
|
|
||||||
let mut args = Vec::new();
|
|
||||||
args.push("build");
|
|
||||||
|
|
||||||
if let Some(tag_value) = tag {
|
|
||||||
args.push("-t");
|
|
||||||
args.push(tag_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(isolation_value) = isolation {
|
|
||||||
args.push("--isolation");
|
|
||||||
args.push(isolation_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("-f");
|
|
||||||
args.push(file);
|
|
||||||
|
|
||||||
args.push(context_dir);
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Updated Module Structure
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// src/virt/buildah/mod.rs
|
|
||||||
mod containers;
|
|
||||||
mod images;
|
|
||||||
mod cmd;
|
|
||||||
mod builder;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod containers_test;
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
/// Error type for buildah operations
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum BuildahError {
|
|
||||||
/// The buildah command failed to execute
|
|
||||||
CommandExecutionFailed(io::Error),
|
|
||||||
/// The buildah command executed but returned an error
|
|
||||||
CommandFailed(String),
|
|
||||||
/// Failed to parse JSON output
|
|
||||||
JsonParseError(String),
|
|
||||||
/// Failed to convert data
|
|
||||||
ConversionError(String),
|
|
||||||
/// Generic error
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for BuildahError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
BuildahError::CommandExecutionFailed(e) => write!(f, "Failed to execute buildah command: {}", e),
|
|
||||||
BuildahError::CommandFailed(e) => write!(f, "Buildah command failed: {}", e),
|
|
||||||
BuildahError::JsonParseError(e) => write!(f, "Failed to parse JSON: {}", e),
|
|
||||||
BuildahError::ConversionError(e) => write!(f, "Conversion error: {}", e),
|
|
||||||
BuildahError::Other(e) => write!(f, "{}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for BuildahError {
|
|
||||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
||||||
match self {
|
|
||||||
BuildahError::CommandExecutionFailed(e) => Some(e),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-export the Builder
|
|
||||||
pub use builder::Builder;
|
|
||||||
|
|
||||||
// Re-export existing functions for backward compatibility
|
|
||||||
#[deprecated(since = "0.2.0", note = "Use Builder::new() instead")]
|
|
||||||
pub use containers::*;
|
|
||||||
#[deprecated(since = "0.2.0", note = "Use Builder methods instead")]
|
|
||||||
pub use images::*;
|
|
||||||
pub use cmd::*;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Rhai Wrapper Changes
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// src/rhai/buildah.rs
|
|
||||||
//! Rhai wrappers for Buildah module functions
|
|
||||||
//!
|
|
||||||
//! This module provides Rhai wrappers for the functions in the Buildah module.
|
|
||||||
|
|
||||||
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::virt::buildah::{self, BuildahError, Image, Builder};
|
|
||||||
use crate::process::CommandResult;
|
|
||||||
|
|
||||||
/// Register Buildah module functions with the Rhai engine
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `engine` - The Rhai engine to register the functions with
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
|
||||||
pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
|
||||||
// Register types
|
|
||||||
register_bah_types(engine)?;
|
|
||||||
|
|
||||||
// Register Builder constructor
|
|
||||||
engine.register_fn("bah_new", bah_new);
|
|
||||||
|
|
||||||
// Register Builder instance methods
|
|
||||||
engine.register_fn("run", |builder: &mut Builder, command: &str| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.run(command))
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("run_with_isolation", |builder: &mut Builder, command: &str, isolation: &str| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.run_with_isolation(command, isolation))
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("copy", |builder: &mut Builder, source: &str, dest: &str| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.copy(source, dest))
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("add", |builder: &mut Builder, source: &str, dest: &str| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.add(source, dest))
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("commit", |builder: &mut Builder, image_name: &str| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.commit(image_name))
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("remove", |builder: &mut Builder| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.remove())
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("config", |builder: &mut Builder, options: Map| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
// Convert Rhai Map to Rust HashMap
|
|
||||||
let config_options = convert_map_to_hashmap(options)?;
|
|
||||||
bah_error_to_rhai_error(builder.config(config_options))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register Builder static methods
|
|
||||||
engine.register_fn("images", |_: &mut Builder| -> Result<Array, Box<EvalAltResult>> {
|
|
||||||
let images = bah_error_to_rhai_error(Builder::images())?;
|
|
||||||
|
|
||||||
// Convert Vec<Image> to Rhai Array
|
|
||||||
let mut array = Array::new();
|
|
||||||
for image in images {
|
|
||||||
array.push(Dynamic::from(image));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(array)
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("image_remove", |_: &mut Builder, image: &str| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::image_remove(image))
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("image_pull", |_: &mut Builder, image: &str, tls_verify: bool| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::image_pull(image, tls_verify))
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("image_push", |_: &mut Builder, image: &str, destination: &str, tls_verify: bool| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify))
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_fn("image_tag", |_: &mut Builder, image: &str, new_name: &str| -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::image_tag(image, new_name))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register legacy functions for backward compatibility
|
|
||||||
register_legacy_functions(engine)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register Buildah module types with the Rhai engine
|
|
||||||
fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
|
||||||
// Register Builder type
|
|
||||||
engine.register_type_with_name::<Builder>("BuildahBuilder");
|
|
||||||
|
|
||||||
// Register getters for Builder properties
|
|
||||||
engine.register_get("container_id", |builder: &mut Builder| {
|
|
||||||
match builder.container_id() {
|
|
||||||
Some(id) => id.clone(),
|
|
||||||
None => "".to_string(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.register_get("name", |builder: &mut Builder| builder.name().to_string());
|
|
||||||
engine.register_get("image", |builder: &mut Builder| builder.image().to_string());
|
|
||||||
|
|
||||||
// Register Image type and methods (same as before)
|
|
||||||
engine.register_type_with_name::<Image>("BuildahImage");
|
|
||||||
|
|
||||||
// Register getters for Image properties
|
|
||||||
engine.register_get("id", |img: &mut Image| img.id.clone());
|
|
||||||
engine.register_get("names", |img: &mut Image| {
|
|
||||||
let mut array = Array::new();
|
|
||||||
for name in &img.names {
|
|
||||||
array.push(Dynamic::from(name.clone()));
|
|
||||||
}
|
|
||||||
array
|
|
||||||
});
|
|
||||||
// Add a 'name' getter that returns the first name or a default
|
|
||||||
engine.register_get("name", |img: &mut Image| {
|
|
||||||
if img.names.is_empty() {
|
|
||||||
"<none>".to_string()
|
|
||||||
} else {
|
|
||||||
img.names[0].clone()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
engine.register_get("size", |img: &mut Image| img.size.clone());
|
|
||||||
engine.register_get("created", |img: &mut Image| img.created.clone());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register legacy functions for backward compatibility
|
|
||||||
fn register_legacy_functions(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
|
||||||
// Register container functions
|
|
||||||
engine.register_fn("bah_from", bah_from_legacy);
|
|
||||||
engine.register_fn("bah_run", bah_run_legacy);
|
|
||||||
engine.register_fn("bah_run_with_isolation", bah_run_with_isolation_legacy);
|
|
||||||
engine.register_fn("bah_copy", bah_copy_legacy);
|
|
||||||
engine.register_fn("bah_add", bah_add_legacy);
|
|
||||||
engine.register_fn("bah_commit", bah_commit_legacy);
|
|
||||||
engine.register_fn("bah_remove", bah_remove_legacy);
|
|
||||||
engine.register_fn("bah_list", bah_list_legacy);
|
|
||||||
engine.register_fn("bah_build", bah_build_with_options_legacy);
|
|
||||||
engine.register_fn("bah_new_build_options", new_build_options);
|
|
||||||
|
|
||||||
// Register image functions
|
|
||||||
engine.register_fn("bah_images", images_legacy);
|
|
||||||
engine.register_fn("bah_image_remove", image_remove_legacy);
|
|
||||||
engine.register_fn("bah_image_push", image_push_legacy);
|
|
||||||
engine.register_fn("bah_image_tag", image_tag_legacy);
|
|
||||||
engine.register_fn("bah_image_pull", image_pull_legacy);
|
|
||||||
engine.register_fn("bah_image_commit", image_commit_with_options_legacy);
|
|
||||||
engine.register_fn("bah_new_commit_options", new_commit_options);
|
|
||||||
engine.register_fn("bah_config", config_with_options_legacy);
|
|
||||||
engine.register_fn("bah_new_config_options", new_config_options);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for error conversion
|
|
||||||
fn bah_error_to_rhai_error<T>(result: Result<T, BuildahError>) -> Result<T, Box<EvalAltResult>> {
|
|
||||||
result.map_err(|e| {
|
|
||||||
Box::new(EvalAltResult::ErrorRuntime(
|
|
||||||
format!("Buildah error: {}", e).into(),
|
|
||||||
rhai::Position::NONE
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert Rhai Map to Rust HashMap
|
|
||||||
fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<EvalAltResult>> {
|
|
||||||
let mut config_options = HashMap::<String, String>::new();
|
|
||||||
|
|
||||||
for (key, value) in options.iter() {
|
|
||||||
if let Ok(value_str) = value.clone().into_string() {
|
|
||||||
// Convert SmartString to String
|
|
||||||
config_options.insert(key.to_string(), value_str);
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
|
||||||
format!("Option '{}' must be a string", key).into(),
|
|
||||||
rhai::Position::NONE
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config_options)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new Builder
|
|
||||||
pub fn bah_new(name: &str, image: &str) -> Result<Builder, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::new(name, image))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy function implementations (for backward compatibility)
|
|
||||||
// These would call the new Builder methods internally
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Example Updates
|
|
||||||
|
|
||||||
#### Rust Example
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// examples/buildah.rs
|
|
||||||
//! Example usage of the buildah module
|
|
||||||
//!
|
|
||||||
//! This file demonstrates how to use the buildah module to perform
|
|
||||||
//! common container operations like creating containers, running commands,
|
|
||||||
//! and managing images.
|
|
||||||
|
|
||||||
use sal::virt::buildah::{Builder, BuildahError};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Run a complete buildah workflow example
|
|
||||||
pub fn run_buildah_example() -> Result<(), BuildahError> {
|
|
||||||
println!("Starting buildah example workflow...");
|
|
||||||
|
|
||||||
// Step 1: Create a container from an image using the Builder
|
|
||||||
println!("\n=== Creating container from fedora:latest ===");
|
|
||||||
let builder = Builder::new("my-fedora-container", "fedora:latest")?;
|
|
||||||
println!("Created container: {}", builder.container_id().unwrap_or(&"unknown".to_string()));
|
|
||||||
|
|
||||||
// Step 2: Run a command in the container
|
|
||||||
println!("\n=== Installing nginx in container ===");
|
|
||||||
// Use chroot isolation to avoid BPF issues
|
|
||||||
let install_result = builder.run_with_isolation("dnf install -y nginx", "chroot")?;
|
|
||||||
println!("{:#?}", install_result);
|
|
||||||
println!("Installation output: {}", install_result.stdout);
|
|
||||||
|
|
||||||
// Step 3: Copy a file into the container
|
|
||||||
println!("\n=== Copying configuration file to container ===");
|
|
||||||
builder.copy("./example.conf", "/etc/example.conf")?;
|
|
||||||
|
|
||||||
// Step 4: Configure container metadata
|
|
||||||
println!("\n=== Configuring container metadata ===");
|
|
||||||
let mut config_options = HashMap::new();
|
|
||||||
config_options.insert("port".to_string(), "80".to_string());
|
|
||||||
config_options.insert("label".to_string(), "maintainer=example@example.com".to_string());
|
|
||||||
config_options.insert("entrypoint".to_string(), "/usr/sbin/nginx".to_string());
|
|
||||||
builder.config(config_options)?;
|
|
||||||
println!("Container configured");
|
|
||||||
|
|
||||||
// Step 5: Commit the container to create a new image
|
|
||||||
println!("\n=== Committing container to create image ===");
|
|
||||||
let image_name = "my-nginx:latest";
|
|
||||||
builder.commit(image_name)?;
|
|
||||||
println!("Created image: {}", image_name);
|
|
||||||
|
|
||||||
// Step 6: List images to verify our new image exists
|
|
||||||
println!("\n=== Listing images ===");
|
|
||||||
let images = Builder::images()?;
|
|
||||||
println!("Found {} images:", images.len());
|
|
||||||
for image in images {
|
|
||||||
println!(" ID: {}", image.id);
|
|
||||||
println!(" Names: {}", image.names.join(", "));
|
|
||||||
println!(" Size: {}", image.size);
|
|
||||||
println!(" Created: {}", image.created);
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 7: Clean up (optional in a real workflow)
|
|
||||||
println!("\n=== Cleaning up ===");
|
|
||||||
Builder::image_remove(image_name)?;
|
|
||||||
builder.remove()?;
|
|
||||||
|
|
||||||
println!("\nBuildah example workflow completed successfully!");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Demonstrate how to build an image from a Containerfile/Dockerfile
|
|
||||||
pub fn build_image_example() -> Result<(), BuildahError> {
|
|
||||||
println!("Building an image from a Containerfile...");
|
|
||||||
|
|
||||||
// Use the build function with tag, context directory, and isolation to avoid BPF issues
|
|
||||||
let result = Builder::build(Some("my-app:latest"), ".", "example_Dockerfile", Some("chroot"))?;
|
|
||||||
|
|
||||||
println!("Build output: {}", result.stdout);
|
|
||||||
println!("Image built successfully!");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Example of pulling and pushing images
|
|
||||||
pub fn registry_operations_example() -> Result<(), BuildahError> {
|
|
||||||
println!("Demonstrating registry operations...");
|
|
||||||
|
|
||||||
// Pull an image
|
|
||||||
println!("\n=== Pulling an image ===");
|
|
||||||
Builder::image_pull("docker.io/library/alpine:latest", true)?;
|
|
||||||
println!("Image pulled successfully");
|
|
||||||
|
|
||||||
// Tag the image
|
|
||||||
println!("\n=== Tagging the image ===");
|
|
||||||
Builder::image_tag("alpine:latest", "my-alpine:v1.0")?;
|
|
||||||
println!("Image tagged successfully");
|
|
||||||
|
|
||||||
// Push an image (this would typically go to a real registry)
|
|
||||||
// println!("\n=== Pushing an image (example only) ===");
|
|
||||||
// println!("In a real scenario, you would push to a registry with:");
|
|
||||||
// println!("Builder::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main function to run all examples
|
|
||||||
pub fn run_all_examples() -> Result<(), BuildahError> {
|
|
||||||
println!("=== BUILDAH MODULE EXAMPLES ===\n");
|
|
||||||
|
|
||||||
run_buildah_example()?;
|
|
||||||
build_image_example()?;
|
|
||||||
registry_operations_example()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let _ = run_all_examples();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Rhai Example
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// rhaiexamples/04_buildah_operations.rhai
|
|
||||||
// Demonstrates container operations using SAL's buildah integration
|
|
||||||
// Note: This script requires buildah to be installed and may need root privileges
|
|
||||||
|
|
||||||
// Check if buildah is installed
|
|
||||||
let buildah_exists = which("buildah");
|
|
||||||
println(`Buildah exists: ${buildah_exists}`);
|
|
||||||
|
|
@ -1,238 +0,0 @@
|
|||||||
# Container Builder Implementation Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document outlines the plan for redesigning the nerdctl interface in the `src/virt/nerdctl` directory to use an object-oriented approach with a Container struct that supports method chaining for the builder pattern. This will replace the existing function-based approach while maintaining all current functionality.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
class Container {
|
|
||||||
-String name
|
|
||||||
-String container_id
|
|
||||||
-String? image
|
|
||||||
-HashMap~String, String~ config
|
|
||||||
-Vec~String~ ports
|
|
||||||
-Vec~String~ volumes
|
|
||||||
-HashMap~String, String~ env_vars
|
|
||||||
-Option~String~ network
|
|
||||||
-Vec~String~ network_aliases
|
|
||||||
-Option~String~ cpu_limit
|
|
||||||
-Option~String~ memory_limit
|
|
||||||
-Option~String~ memory_swap_limit
|
|
||||||
-Option~String~ cpu_shares
|
|
||||||
-Option~String~ restart_policy
|
|
||||||
-Option~HealthCheck~ health_check
|
|
||||||
+new(name: &str) -> Result~Container, NerdctlError~
|
|
||||||
+from_image(name: &str, image: &str) -> Result~Container, NerdctlError~
|
|
||||||
+with_port(port: &str) -> Container
|
|
||||||
+with_ports(ports: &[&str]) -> Container
|
|
||||||
+with_volume(volume: &str) -> Container
|
|
||||||
+with_volumes(volumes: &[&str]) -> Container
|
|
||||||
+with_env(key: &str, value: &str) -> Container
|
|
||||||
+with_envs(env_map: &HashMap<&str, &str>) -> Container
|
|
||||||
+with_network(network: &str) -> Container
|
|
||||||
+with_network_alias(alias: &str) -> Container
|
|
||||||
+with_network_aliases(aliases: &[&str]) -> Container
|
|
||||||
+with_cpu_limit(cpus: &str) -> Container
|
|
||||||
+with_memory_limit(memory: &str) -> Container
|
|
||||||
+with_memory_swap_limit(memory_swap: &str) -> Container
|
|
||||||
+with_cpu_shares(shares: &str) -> Container
|
|
||||||
+with_restart_policy(policy: &str) -> Container
|
|
||||||
+with_health_check(cmd: &str) -> Container
|
|
||||||
+with_health_check_options(cmd: &str, interval: Option<&str>, timeout: Option<&str>, retries: Option<u32>, start_period: Option<&str>) -> Container
|
|
||||||
+with_snapshotter(snapshotter: &str) -> Container
|
|
||||||
+with_detach(detach: bool) -> Container
|
|
||||||
+build() -> Result~Container, NerdctlError~
|
|
||||||
+start() -> Result~CommandResult, NerdctlError~
|
|
||||||
+stop() -> Result~CommandResult, NerdctlError~
|
|
||||||
+remove() -> Result~CommandResult, NerdctlError~
|
|
||||||
+exec(command: &str) -> Result~CommandResult, NerdctlError~
|
|
||||||
+copy(source: &str, dest: &str) -> Result~CommandResult, NerdctlError~
|
|
||||||
+export(path: &str) -> Result~CommandResult, NerdctlError~
|
|
||||||
+commit(image_name: &str) -> Result~CommandResult, NerdctlError~
|
|
||||||
+status() -> Result~ContainerStatus, NerdctlError~
|
|
||||||
+health_status() -> Result~String, NerdctlError~
|
|
||||||
+resources() -> Result~ResourceUsage, NerdctlError~
|
|
||||||
}
|
|
||||||
|
|
||||||
class HealthCheck {
|
|
||||||
+String cmd
|
|
||||||
+Option~String~ interval
|
|
||||||
+Option~String~ timeout
|
|
||||||
+Option~u32~ retries
|
|
||||||
+Option~String~ start_period
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContainerStatus {
|
|
||||||
+String state
|
|
||||||
+String status
|
|
||||||
+String created
|
|
||||||
+String started
|
|
||||||
+Option~String~ health_status
|
|
||||||
+Option~String~ health_output
|
|
||||||
}
|
|
||||||
|
|
||||||
class ResourceUsage {
|
|
||||||
+String cpu_usage
|
|
||||||
+String memory_usage
|
|
||||||
+String memory_limit
|
|
||||||
+String memory_percentage
|
|
||||||
+String network_input
|
|
||||||
+String network_output
|
|
||||||
+String block_input
|
|
||||||
+String block_output
|
|
||||||
+String pids
|
|
||||||
}
|
|
||||||
|
|
||||||
class NerdctlError {
|
|
||||||
+CommandExecutionFailed(io::Error)
|
|
||||||
+CommandFailed(String)
|
|
||||||
+JsonParseError(String)
|
|
||||||
+ConversionError(String)
|
|
||||||
+Other(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
Container --> ContainerStatus : returns
|
|
||||||
Container --> ResourceUsage : returns
|
|
||||||
Container --> HealthCheck : contains
|
|
||||||
Container --> NerdctlError : may throw
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Steps
|
|
||||||
|
|
||||||
### 1. Create Container Struct and Basic Methods
|
|
||||||
|
|
||||||
Create a new file `src/virt/nerdctl/container.rs` with the Container struct and basic methods.
|
|
||||||
|
|
||||||
### 2. Implement Builder Pattern Methods
|
|
||||||
|
|
||||||
Add builder pattern methods to the Container struct for configuration.
|
|
||||||
|
|
||||||
### 3. Implement Container Operations
|
|
||||||
|
|
||||||
Add methods for container operations like start, stop, exec, etc.
|
|
||||||
|
|
||||||
### 4. Implement Status and Resource Usage Methods
|
|
||||||
|
|
||||||
Add methods for getting container status and resource usage information.
|
|
||||||
|
|
||||||
### 5. Update mod.rs to Export the New Container Struct
|
|
||||||
|
|
||||||
Update `src/virt/nerdctl/mod.rs` to include the new container module.
|
|
||||||
|
|
||||||
### 6. Create Example Usage
|
|
||||||
|
|
||||||
Create an example file to demonstrate the new Container API.
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### Container Creation and Configuration
|
|
||||||
|
|
||||||
- Method chaining for the builder pattern
|
|
||||||
- Support for multiple ports and volumes
|
|
||||||
- Environment variable configuration
|
|
||||||
- Network configuration and aliases
|
|
||||||
- Resource limits (CPU, memory)
|
|
||||||
- Restart policies
|
|
||||||
- Health checks
|
|
||||||
|
|
||||||
### Container Operations
|
|
||||||
|
|
||||||
- Start, stop, and remove containers
|
|
||||||
- Execute commands in containers
|
|
||||||
- Copy files between container and host
|
|
||||||
- Export containers to tarballs
|
|
||||||
- Commit containers to images
|
|
||||||
|
|
||||||
### Container Monitoring
|
|
||||||
|
|
||||||
- Get container status information
|
|
||||||
- Get container health status
|
|
||||||
- Get resource usage information
|
|
||||||
|
|
||||||
## Example Usage
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Create a container with various configuration options
|
|
||||||
let container = Container::from_image("my-web-app", "nginx:latest")?
|
|
||||||
.with_ports(&["8080:80", "8443:443"])
|
|
||||||
.with_volumes(&[
|
|
||||||
"./html:/usr/share/nginx/html",
|
|
||||||
"./config/nginx.conf:/etc/nginx/nginx.conf"
|
|
||||||
])
|
|
||||||
.with_env("NGINX_HOST", "example.com")
|
|
||||||
.with_env("NGINX_PORT", "80")
|
|
||||||
.with_network("app-network")
|
|
||||||
.with_network_alias("web")
|
|
||||||
.with_cpu_limit("0.5")
|
|
||||||
.with_memory_limit("512m")
|
|
||||||
.with_restart_policy("always")
|
|
||||||
.with_health_check_options(
|
|
||||||
"curl -f http://localhost/ || exit 1",
|
|
||||||
Some("10s"),
|
|
||||||
Some("5s"),
|
|
||||||
Some(3),
|
|
||||||
Some("30s")
|
|
||||||
)
|
|
||||||
.with_detach(true)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
// Start the container
|
|
||||||
container.start()?;
|
|
||||||
|
|
||||||
// Execute a command in the container
|
|
||||||
let result = container.exec("echo 'Hello from container'")?;
|
|
||||||
println!("Command output: {}", result.stdout);
|
|
||||||
|
|
||||||
// Get container status
|
|
||||||
let status = container.status()?;
|
|
||||||
println!("Container state: {}", status.state);
|
|
||||||
println!("Container status: {}", status.status);
|
|
||||||
|
|
||||||
// Get resource usage
|
|
||||||
let resources = container.resources()?;
|
|
||||||
println!("CPU usage: {}", resources.cpu_usage);
|
|
||||||
println!("Memory usage: {}", resources.memory_usage);
|
|
||||||
|
|
||||||
// Stop and remove the container
|
|
||||||
container.stop()?;
|
|
||||||
container.remove()?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Network Management
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Create a network
|
|
||||||
Container::create_network("app-network", Some("bridge"))?;
|
|
||||||
|
|
||||||
// Create containers in the network
|
|
||||||
let db = Container::from_image("db", "postgres:latest")?
|
|
||||||
.with_network("app-network")
|
|
||||||
.with_network_alias("database")
|
|
||||||
.with_env("POSTGRES_PASSWORD", "example")
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let app = Container::from_image("app", "my-app:latest")?
|
|
||||||
.with_network("app-network")
|
|
||||||
.with_env("DATABASE_URL", "postgres://postgres:example@database:5432/postgres")
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
// Remove the network when done
|
|
||||||
Container::remove_network("app-network")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
1. Create the new Container struct and its methods
|
|
||||||
2. Update the mod.rs file to export the new Container struct
|
|
||||||
3. Create example usage to demonstrate the new API
|
|
||||||
4. Deprecate the old function-based API (but keep it for backward compatibility)
|
|
||||||
5. Update documentation to reflect the new API
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
1. Unit tests for the Container struct and its methods
|
|
||||||
2. Integration tests for the Container API
|
|
||||||
3. Manual testing with real containers
|
|
115
examples/buildah.rs
Normal file
115
examples/buildah.rs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
//! Example usage of the buildah module
|
||||||
|
//!
|
||||||
|
//! This file demonstrates how to use the buildah module to perform
|
||||||
|
//! common container operations like creating containers, running commands,
|
||||||
|
//! and managing images.
|
||||||
|
|
||||||
|
use sal::virt::buildah::{self, BuildahError};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Run a complete buildah workflow example
|
||||||
|
pub fn run_buildah_example() -> Result<(), BuildahError> {
|
||||||
|
println!("Starting buildah example workflow...");
|
||||||
|
|
||||||
|
// Step 1: Create a container from an image
|
||||||
|
println!("\n=== Creating container from fedora:latest ===");
|
||||||
|
let result = buildah::from("fedora:latest")?;
|
||||||
|
let container_id = result.stdout.trim();
|
||||||
|
println!("Created container: {}", container_id);
|
||||||
|
|
||||||
|
// Step 2: Run a command in the container
|
||||||
|
println!("\n=== Installing nginx in container ===");
|
||||||
|
// Use chroot isolation to avoid BPF issues
|
||||||
|
let install_result = buildah::bah_run_with_isolation(container_id, "dnf install -y nginx", "chroot")?;
|
||||||
|
println!("{:#?}", install_result);
|
||||||
|
println!("Installation output: {}", install_result.stdout);
|
||||||
|
|
||||||
|
// Step 3: Copy a file into the container
|
||||||
|
println!("\n=== Copying configuration file to container ===");
|
||||||
|
buildah::bah_copy(container_id, "./example.conf", "/etc/example.conf").unwrap();
|
||||||
|
|
||||||
|
// Step 4: Configure container metadata
|
||||||
|
println!("\n=== Configuring container metadata ===");
|
||||||
|
let mut config_options = HashMap::new();
|
||||||
|
config_options.insert("port".to_string(), "80".to_string());
|
||||||
|
config_options.insert("label".to_string(), "maintainer=example@example.com".to_string());
|
||||||
|
config_options.insert("entrypoint".to_string(), "/usr/sbin/nginx".to_string());
|
||||||
|
buildah::bah_config(container_id, config_options)?;
|
||||||
|
buildah::config(container_id, config_options)?;
|
||||||
|
println!("Container configured");
|
||||||
|
|
||||||
|
// Step 5: Commit the container to create a new image
|
||||||
|
println!("\n=== Committing container to create image ===");
|
||||||
|
let image_name = "my-nginx:latest";
|
||||||
|
buildah::image_commit(container_id, image_name, Some("docker"), true, true)?;
|
||||||
|
println!("Created image: {}", image_name);
|
||||||
|
|
||||||
|
// Step 6: List images to verify our new image exists
|
||||||
|
println!("\n=== Listing images ===");
|
||||||
|
let images = buildah::images()?;
|
||||||
|
println!("Found {} images:", images.len());
|
||||||
|
for image in images {
|
||||||
|
println!(" ID: {}", image.id);
|
||||||
|
println!(" Names: {}", image.names.join(", "));
|
||||||
|
println!(" Size: {}", image.size);
|
||||||
|
println!(" Created: {}", image.created);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Step 7: Clean up (optional in a real workflow)
|
||||||
|
println!("\n=== Cleaning up ===");
|
||||||
|
buildah::image_remove(image_name).unwrap();
|
||||||
|
|
||||||
|
println!("\nBuildah example workflow completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Demonstrate how to build an image from a Containerfile/Dockerfile
|
||||||
|
pub fn build_image_example() -> Result<(), BuildahError> {
|
||||||
|
println!("Building an image from a Containerfile...");
|
||||||
|
|
||||||
|
// Use the build function with tag, context directory, and isolation to avoid BPF issues
|
||||||
|
let result = buildah::bah_build(Some("my-app:latest"), ".", "example_Dockerfile", Some("chroot"))?;
|
||||||
|
|
||||||
|
println!("Build output: {}", result.stdout);
|
||||||
|
println!("Image built successfully!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Example of pulling and pushing images
|
||||||
|
pub fn registry_operations_example() -> Result<(), BuildahError> {
|
||||||
|
println!("Demonstrating registry operations...");
|
||||||
|
|
||||||
|
// Pull an image
|
||||||
|
println!("\n=== Pulling an image ===");
|
||||||
|
buildah::image_pull("docker.io/library/alpine:latest", true)?;
|
||||||
|
println!("Image pulled successfully");
|
||||||
|
|
||||||
|
// Tag the image
|
||||||
|
println!("\n=== Tagging the image ===");
|
||||||
|
buildah::image_tag("alpine:latest", "my-alpine:v1.0")?;
|
||||||
|
println!("Image tagged successfully");
|
||||||
|
|
||||||
|
// Push an image (this would typically go to a real registry)
|
||||||
|
// println!("\n=== Pushing an image (example only) ===");
|
||||||
|
// println!("In a real scenario, you would push to a registry with:");
|
||||||
|
// println!("buildah::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main function to run all examples
|
||||||
|
pub fn run_all_examples() -> Result<(), BuildahError> {
|
||||||
|
println!("=== BUILDAH MODULE EXAMPLES ===\n");
|
||||||
|
|
||||||
|
run_buildah_example()?;
|
||||||
|
build_image_example()?;
|
||||||
|
registry_operations_example()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let _ = run_all_examples().unwrap();
|
||||||
|
}
|
@ -1,62 +0,0 @@
|
|||||||
// File: /root/code/git.ourworld.tf/herocode/sal/examples/container_example.rs
|
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
use sal::virt::nerdctl::Container;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
// Create a container from an image
|
|
||||||
println!("Creating container from image...");
|
|
||||||
let container = Container::from_image("my-nginx", "nginx:latest")?
|
|
||||||
.with_port("8080:80")
|
|
||||||
.with_env("NGINX_HOST", "example.com")
|
|
||||||
.with_volume("/tmp/nginx:/usr/share/nginx/html")
|
|
||||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
|
||||||
.with_detach(true)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
println!("Container created successfully");
|
|
||||||
|
|
||||||
// Execute a command in the container
|
|
||||||
println!("Executing command in container...");
|
|
||||||
let result = container.exec("echo 'Hello from container'")?;
|
|
||||||
println!("Command output: {}", result.stdout);
|
|
||||||
|
|
||||||
// Get container status
|
|
||||||
println!("Getting container status...");
|
|
||||||
let status = container.status()?;
|
|
||||||
println!("Container status: {}", status.status);
|
|
||||||
|
|
||||||
// Get resource usage
|
|
||||||
println!("Getting resource usage...");
|
|
||||||
let resources = container.resources()?;
|
|
||||||
println!("CPU usage: {}", resources.cpu_usage);
|
|
||||||
println!("Memory usage: {}", resources.memory_usage);
|
|
||||||
|
|
||||||
// Stop and remove the container
|
|
||||||
println!("Stopping and removing container...");
|
|
||||||
container.stop()?;
|
|
||||||
container.remove()?;
|
|
||||||
|
|
||||||
println!("Container stopped and removed");
|
|
||||||
|
|
||||||
// Get a container by name (if it exists)
|
|
||||||
println!("\nGetting a container by name...");
|
|
||||||
match Container::new("existing-container") {
|
|
||||||
Ok(container) => {
|
|
||||||
if container.container_id.is_some() {
|
|
||||||
println!("Found container with ID: {}", container.container_id.unwrap());
|
|
||||||
|
|
||||||
// Perform operations on the existing container
|
|
||||||
let status = container.status()?;
|
|
||||||
println!("Container status: {}", status.status);
|
|
||||||
} else {
|
|
||||||
println!("Container exists but has no ID");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error getting container: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
84
examples/nerdctl.rs
Normal file
84
examples/nerdctl.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//! Example usage of the nerdctl module
|
||||||
|
//!
|
||||||
|
//! This file demonstrates how to use the nerdctl module to perform
|
||||||
|
//! common container operations like creating containers, running commands,
|
||||||
|
//! and managing images.
|
||||||
|
|
||||||
|
use sal::virt::nerdctl::{self, NerdctlError};
|
||||||
|
|
||||||
|
/// Run a complete nerdctl workflow example
|
||||||
|
pub fn run_nerdctl_example() -> Result<(), NerdctlError> {
|
||||||
|
println!("Starting nerdctl example workflow...");
|
||||||
|
|
||||||
|
// Step 1: Pull an image
|
||||||
|
println!("\n=== Pulling nginx:latest image ===");
|
||||||
|
let pull_result = nerdctl::image_pull("nginx:latest")?;
|
||||||
|
println!("Pull output: {}", pull_result.stdout);
|
||||||
|
|
||||||
|
// Step 2: Create a container from the image
|
||||||
|
println!("\n=== Creating container from nginx:latest ===");
|
||||||
|
// Use "native" snapshotter to avoid overlay mount issues
|
||||||
|
let run_result = nerdctl::run("nginx:latest", Some("my-nginx"), true, Some(&["8080:80"]), Some("native"))?;
|
||||||
|
println!("Container created: {}", run_result.stdout.trim());
|
||||||
|
let container_id = "my-nginx"; // Using the name we specified
|
||||||
|
|
||||||
|
// Step 3: Execute a command in the container
|
||||||
|
println!("\n=== Installing curl in container ===");
|
||||||
|
let update_result = nerdctl::exec(container_id, "apt-get update")?;
|
||||||
|
println!("Update output: {}", update_result.stdout);
|
||||||
|
|
||||||
|
let install_result = nerdctl::exec(container_id, "apt-get install -y curl")?;
|
||||||
|
println!("Installation output: {}", install_result.stdout);
|
||||||
|
|
||||||
|
// Step 4: Copy a file into the container (assuming nginx.conf exists)
|
||||||
|
println!("\n=== Copying configuration file to container ===");
|
||||||
|
nerdctl::copy("./nginx.conf", format!("{}:/etc/nginx/nginx.conf", container_id).as_str())?;
|
||||||
|
|
||||||
|
// Step 5: Commit the container to create a new image
|
||||||
|
println!("\n=== Committing container to create image ===");
|
||||||
|
let image_name = "my-custom-nginx:latest";
|
||||||
|
nerdctl::image_commit(container_id, image_name)?;
|
||||||
|
println!("Created image: {}", image_name);
|
||||||
|
|
||||||
|
// Step 6: Stop and remove the container
|
||||||
|
println!("\n=== Stopping and removing container ===");
|
||||||
|
nerdctl::stop(container_id)?;
|
||||||
|
nerdctl::remove(container_id)?;
|
||||||
|
println!("Container stopped and removed");
|
||||||
|
|
||||||
|
// Step 7: Create a new container from our custom image
|
||||||
|
println!("\n=== Creating container from custom image ===");
|
||||||
|
// Use "native" snapshotter to avoid overlay mount issues
|
||||||
|
nerdctl::run(image_name, Some("nginx-custom"), true, Some(&["8081:80"]), Some("native"))?;
|
||||||
|
println!("Custom container created");
|
||||||
|
|
||||||
|
// Step 8: List images
|
||||||
|
println!("\n=== Listing images ===");
|
||||||
|
let images_result = nerdctl::images()?;
|
||||||
|
println!("Images: \n{}", images_result.stdout);
|
||||||
|
|
||||||
|
// Step 9: Clean up (optional in a real workflow)
|
||||||
|
println!("\n=== Cleaning up ===");
|
||||||
|
nerdctl::stop("nginx-custom")?;
|
||||||
|
nerdctl::remove("nginx-custom")?;
|
||||||
|
nerdctl::image_remove(image_name)?;
|
||||||
|
|
||||||
|
println!("\nNerdctl example workflow completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main function to run all examples
|
||||||
|
pub fn run_all_examples() -> Result<(), NerdctlError> {
|
||||||
|
println!("=== NERDCTL MODULE EXAMPLES ===\n");
|
||||||
|
|
||||||
|
run_nerdctl_example()?;
|
||||||
|
|
||||||
|
println!("\nNote that these examples require nerdctl to be installed on your system");
|
||||||
|
println!("and may require root/sudo privileges depending on your setup.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let _ = run_all_examples().unwrap();
|
||||||
|
}
|
74
examples/rhai_buildah_example.rs
Normal file
74
examples/rhai_buildah_example.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
//! Example of using the Rhai integration with SAL Buildah module
|
||||||
|
//!
|
||||||
|
//! This example demonstrates how to use the Rhai scripting language
|
||||||
|
//! with the System Abstraction Layer (SAL) Buildah module.
|
||||||
|
|
||||||
|
use rhai::Engine;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
// Create a new Rhai engine
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
// Register println function
|
||||||
|
engine.register_fn("println", |s: &str| println!("{}", s));
|
||||||
|
|
||||||
|
// Register SAL functions with the engine
|
||||||
|
sal::rhai::register(&mut engine)?;
|
||||||
|
|
||||||
|
// Run a Rhai script that uses SAL Buildah functions
|
||||||
|
let script = r#"
|
||||||
|
// List available images
|
||||||
|
println("Listing available images:");
|
||||||
|
let images = buildah_images();
|
||||||
|
println("Found " + images.len() + " images");
|
||||||
|
|
||||||
|
// Create a container from an image (uncomment if you have a valid image)
|
||||||
|
// let container = buildah_from("alpine:latest");
|
||||||
|
// println("Created container: " + container.stdout.trim());
|
||||||
|
|
||||||
|
// Build an image using options
|
||||||
|
let build_options = buildah_new_build_options();
|
||||||
|
build_options.tag = "example-image:latest";
|
||||||
|
build_options.context_dir = ".";
|
||||||
|
build_options.file = "example_Dockerfile";
|
||||||
|
|
||||||
|
println("Building image with options:");
|
||||||
|
println(" Tag: " + build_options.tag);
|
||||||
|
println(" Context: " + build_options.context_dir);
|
||||||
|
println(" Dockerfile: " + build_options.file);
|
||||||
|
|
||||||
|
// Uncomment to actually build the image
|
||||||
|
// let build_result = buildah_build(build_options);
|
||||||
|
// println("Build result: " + build_result.success);
|
||||||
|
|
||||||
|
// Create a container configuration
|
||||||
|
let config_options = buildah_new_config_options();
|
||||||
|
config_options.author = "Rhai Example";
|
||||||
|
config_options.cmd = "/bin/sh -c 'echo Hello from Buildah'";
|
||||||
|
|
||||||
|
println("Container config options:");
|
||||||
|
println(" Author: " + config_options.author);
|
||||||
|
println(" Command: " + config_options.cmd);
|
||||||
|
|
||||||
|
// Commit options
|
||||||
|
let commit_options = buildah_new_commit_options();
|
||||||
|
commit_options.format = "docker";
|
||||||
|
commit_options.squash = true;
|
||||||
|
commit_options.rm = true;
|
||||||
|
|
||||||
|
println("Commit options:");
|
||||||
|
println(" Format: " + commit_options.format);
|
||||||
|
println(" Squash: " + commit_options.squash);
|
||||||
|
println(" Remove container: " + commit_options.rm);
|
||||||
|
|
||||||
|
// Return success
|
||||||
|
true
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// Evaluate the script
|
||||||
|
let result = engine.eval::<bool>(script)?;
|
||||||
|
println!("Script execution successful: {}", result);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
54
examples/rhai_process_example.rs
Normal file
54
examples/rhai_process_example.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//! Example of using the Rhai integration with SAL Process module
|
||||||
|
//!
|
||||||
|
//! This example demonstrates how to use the Rhai scripting language
|
||||||
|
//! with the System Abstraction Layer (SAL) Process module.
|
||||||
|
|
||||||
|
use rhai::Engine;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
// Create a new Rhai engine
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
// Register SAL functions with the engine
|
||||||
|
sal::rhai::register(&mut engine)?;
|
||||||
|
|
||||||
|
// Run a Rhai script that uses SAL Process functions
|
||||||
|
let script = r#"
|
||||||
|
// Check if a command exists
|
||||||
|
let ls_exists = which("ls");
|
||||||
|
println("ls command exists: " + ls_exists);
|
||||||
|
|
||||||
|
// Run a simple command
|
||||||
|
let echo_result = run_command("echo 'Hello from Rhai!'");
|
||||||
|
println("Echo command output: " + echo_result.stdout);
|
||||||
|
println("Echo command success: " + echo_result.success);
|
||||||
|
|
||||||
|
// Run a command silently
|
||||||
|
let silent_result = run_silent("ls -la");
|
||||||
|
println("Silent command success: " + silent_result.success);
|
||||||
|
|
||||||
|
// Run a command with custom options using a Map
|
||||||
|
let options = new_run_options();
|
||||||
|
options["die"] = false; // Don't return error if command fails
|
||||||
|
options["silent"] = true; // Suppress output to stdout/stderr
|
||||||
|
options["async_exec"] = false; // Run synchronously
|
||||||
|
options["log"] = true; // Log command execution
|
||||||
|
|
||||||
|
let custom_result = run("echo 'Custom options'", options);
|
||||||
|
println("Custom command success: " + custom_result.success);
|
||||||
|
|
||||||
|
// List processes
|
||||||
|
let processes = process_list("");
|
||||||
|
println("Number of processes: " + processes.len());
|
||||||
|
|
||||||
|
// Return success
|
||||||
|
true
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// Evaluate the script
|
||||||
|
let result = engine.eval::<bool>(script)?;
|
||||||
|
println!("Script execution successful: {}", result);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
96
rhaiexamples/04_buildah_operations.rhai
Normal file
96
rhaiexamples/04_buildah_operations.rhai
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// 04_buildah_operations.rhai
|
||||||
|
// Demonstrates container operations using SAL's buildah integration
|
||||||
|
// Note: This script requires buildah to be installed and may need root privileges
|
||||||
|
|
||||||
|
// Check if buildah is installed
|
||||||
|
let buildah_exists = which("buildah");
|
||||||
|
println(`Buildah exists: ${buildah_exists}`);
|
||||||
|
|
||||||
|
// List available images (only if buildah is installed)
|
||||||
|
println("Listing available container images:");
|
||||||
|
// if ! buildah_exists != "" {
|
||||||
|
// //EXIT
|
||||||
|
// }
|
||||||
|
let images = bah_images();
|
||||||
|
println(`Found ${images.len()} images`);
|
||||||
|
|
||||||
|
// Print image details (limited to 3)
|
||||||
|
let count = 0;
|
||||||
|
for img in images {
|
||||||
|
if count >= 3 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
println(` - ID: ${img.id}, Name: ${img.name}, Created: ${img.created}`);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a container from an image
|
||||||
|
println("\nCreating a container from alpine image:");
|
||||||
|
let container = bah_from("alpine:latest");
|
||||||
|
println(`Container result: success=${container.success}, code=${container.code}`);
|
||||||
|
println(`Container stdout: "${container.stdout}"`);
|
||||||
|
println(`Container stderr: "${container.stderr}"`);
|
||||||
|
let container_id = container.stdout;
|
||||||
|
println(`Container ID: ${container_id}`);
|
||||||
|
|
||||||
|
//Run a command in the container
|
||||||
|
println("\nRunning a command in the container:");
|
||||||
|
let run_result = bah_run(container_id, "echo 'Hello from container'");
|
||||||
|
println(`Command output: ${run_result.stdout}`);
|
||||||
|
|
||||||
|
//Add a file to the container
|
||||||
|
println("\nAdding a file to the container:");
|
||||||
|
let test_file = "test_file.txt";
|
||||||
|
run(`echo "Test content" > ${test_file}`);
|
||||||
|
let add_result = bah_add(container_id, test_file, "/");
|
||||||
|
println(`Add result: ${add_result.success}`);
|
||||||
|
|
||||||
|
//Commit the container to create a new image
|
||||||
|
println("\nCommitting the container to create a new image:");
|
||||||
|
let commit_result = bah_commit(container_id, "my-custom-image:latest");
|
||||||
|
println(`Commit result: ${commit_result.success}`);
|
||||||
|
|
||||||
|
//Remove the container
|
||||||
|
println("\nRemoving the container:");
|
||||||
|
let remove_result = bah_remove(container_id);
|
||||||
|
println(`Remove result: ${remove_result.success}`);
|
||||||
|
|
||||||
|
//Clean up the test file
|
||||||
|
delete(test_file);
|
||||||
|
|
||||||
|
// Demonstrate build options
|
||||||
|
println("\nDemonstrating build options:");
|
||||||
|
let build_options = bah_new_build_options();
|
||||||
|
build_options.tag = "example-image:latest";
|
||||||
|
build_options.context_dir = ".";
|
||||||
|
build_options.file = "example_Dockerfile";
|
||||||
|
|
||||||
|
println("Build options configured:");
|
||||||
|
println(` - Tag: ${build_options.tag}`);
|
||||||
|
println(` - Context: ${build_options.context_dir}`);
|
||||||
|
println(` - Dockerfile: ${build_options.file}`);
|
||||||
|
|
||||||
|
// Demonstrate commit options
|
||||||
|
println("\nDemonstrating commit options:");
|
||||||
|
let commit_options = bah_new_commit_options();
|
||||||
|
commit_options.format = "docker";
|
||||||
|
commit_options.squash = true;
|
||||||
|
commit_options.rm = true;
|
||||||
|
|
||||||
|
println("Commit options configured:");
|
||||||
|
println(` - Format: ${commit_options.format}`);
|
||||||
|
println(` - Squash: ${commit_options.squash}`);
|
||||||
|
println(` - Remove container: ${commit_options.rm}`);
|
||||||
|
|
||||||
|
// Demonstrate config options
|
||||||
|
println("\nDemonstrating config options:");
|
||||||
|
let config_options = bah_new_config_options();
|
||||||
|
config_options.author = "Rhai Example";
|
||||||
|
config_options.cmd = "/bin/sh -c 'echo Hello from Buildah'";
|
||||||
|
|
||||||
|
println("Config options configured:");
|
||||||
|
println(` - Author: ${config_options.author}`);
|
||||||
|
println(` - Command: ${config_options.cmd}`);
|
||||||
|
|
||||||
|
|
||||||
|
"Buildah operations script completed successfully!"
|
135
rhaiexamples/git_match_test.rhai
Normal file
135
rhaiexamples/git_match_test.rhai
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// Test script for Git partial path matching functionality
|
||||||
|
|
||||||
|
print("===== Git Path Matching Test =====");
|
||||||
|
|
||||||
|
// Test git availability
|
||||||
|
print("\n=== Checking Git Availability ===");
|
||||||
|
let git_cmd = which("git");
|
||||||
|
if git_cmd != false {
|
||||||
|
print(`Git is available at: ${git_cmd}`);
|
||||||
|
} else {
|
||||||
|
print("WARNING: Git is not installed. Tests will be skipped.");
|
||||||
|
return "Git not available - test skipped";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for test assertions
|
||||||
|
fn assert(condition, message) {
|
||||||
|
if (condition == false) {
|
||||||
|
print(`FAILED: ${message}`);
|
||||||
|
throw `Assertion failed: ${message}`;
|
||||||
|
} else {
|
||||||
|
print(`PASSED: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n=== Setting up test repositories ===");
|
||||||
|
// Get current repos from git_list
|
||||||
|
let repos = git_list();
|
||||||
|
print(`Found ${repos.len()} local git repositories for testing`);
|
||||||
|
|
||||||
|
if repos.len() == 0 {
|
||||||
|
print("No repositories found for testing. Creating a test repo...");
|
||||||
|
// Create a test repo in a temporary directory
|
||||||
|
let test_dir = "~/tmp_test_repo";
|
||||||
|
print(`Creating test directory: ${test_dir}`);
|
||||||
|
|
||||||
|
// Initialize a git repo with run commands
|
||||||
|
let result = run_silent(`
|
||||||
|
mkdir -p ${test_dir}/test1
|
||||||
|
cd ${test_dir}/test1
|
||||||
|
git init
|
||||||
|
echo "Test content" > README.md
|
||||||
|
git add README.md
|
||||||
|
git config --global user.email "test@example.com"
|
||||||
|
git config --global user.name "Test User"
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
`);
|
||||||
|
|
||||||
|
if result.success {
|
||||||
|
print("Created test repository successfully");
|
||||||
|
// Update the repos list
|
||||||
|
repos = git_list();
|
||||||
|
print(`Now found ${repos.len()} local git repositories`);
|
||||||
|
} else {
|
||||||
|
print("Failed to create test repository");
|
||||||
|
return "Test setup failed - could not create test repository";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple function to get just the repo name from the path for easier debugging
|
||||||
|
fn get_repo_name(path) {
|
||||||
|
let parts = path.split("/");
|
||||||
|
return parts[parts.len()-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n=== Test 1: Testing git_update with exact path ===");
|
||||||
|
// Using the first repo in the list for testing
|
||||||
|
let first_repo = repos[0];
|
||||||
|
print(`Using repository: ${first_repo}`);
|
||||||
|
print(`Repository name: ${get_repo_name(first_repo)}`);
|
||||||
|
|
||||||
|
// Test with exact path
|
||||||
|
let exact_result = git_update(first_repo);
|
||||||
|
print(`Result with exact path: ${exact_result}`);
|
||||||
|
|
||||||
|
// Check if the result was as expected
|
||||||
|
// Note: The function might find multiple repos even with exact path
|
||||||
|
// so we also check for that case
|
||||||
|
let is_success = exact_result.contains("up to date") ||
|
||||||
|
exact_result.contains("updated") ||
|
||||||
|
exact_result.contains("has local changes");
|
||||||
|
|
||||||
|
let multiple_match = exact_result.contains("Multiple repositories");
|
||||||
|
|
||||||
|
assert(is_success || multiple_match,
|
||||||
|
"git_update with path should succeed, indicate local changes, or report multiple matches");
|
||||||
|
|
||||||
|
print("\n=== Test 2: Testing git_update with partial path ===");
|
||||||
|
// Extract part of the path (last part of the path)
|
||||||
|
let repo_name = get_repo_name(first_repo);
|
||||||
|
print(`Testing partial match with: ${repo_name}`);
|
||||||
|
let partial_result = git_update(repo_name);
|
||||||
|
print(`Result with partial path: ${partial_result}`);
|
||||||
|
|
||||||
|
// Check if the result was as expected - similar to exact path test
|
||||||
|
let partial_success = partial_result.contains("up to date") ||
|
||||||
|
partial_result.contains("updated") ||
|
||||||
|
partial_result.contains("has local changes");
|
||||||
|
|
||||||
|
let partial_multiple = partial_result.contains("Multiple repositories");
|
||||||
|
|
||||||
|
assert(partial_success || partial_multiple,
|
||||||
|
"git_update with partial path should succeed, indicate local changes, or report multiple matches");
|
||||||
|
|
||||||
|
print("\n=== Test 3: Testing git_update with non-existent path ===");
|
||||||
|
let fake_repo = "this_repo_does_not_exist_anywhere";
|
||||||
|
print(`Testing with non-existent path: ${fake_repo}`);
|
||||||
|
let nonexist_result = git_update(fake_repo);
|
||||||
|
print(`Result with non-existent path: ${nonexist_result}`);
|
||||||
|
|
||||||
|
// Check that it properly reports an error
|
||||||
|
assert(nonexist_result.contains("No repositories found"),
|
||||||
|
"git_update with non-existent path should indicate no matching repos");
|
||||||
|
|
||||||
|
print("\n=== Test 4: Testing wildcard matching ===");
|
||||||
|
// Try to find a common substring in multiple repos
|
||||||
|
// For this test, we'll use a known path pattern likely to match multiple repos
|
||||||
|
let common_part = "code";
|
||||||
|
print(`Testing wildcard match with: ${common_part}*`);
|
||||||
|
|
||||||
|
let wildcard_result = git_update(common_part + "*");
|
||||||
|
print(`Result with wildcard: ${wildcard_result}`);
|
||||||
|
|
||||||
|
// For wildcard, we expect at least one match but not an error
|
||||||
|
// Implementation might return the first match or a success message
|
||||||
|
let wildcard_success = wildcard_result.contains("up to date") ||
|
||||||
|
wildcard_result.contains("updated") ||
|
||||||
|
wildcard_result.contains("has local changes");
|
||||||
|
|
||||||
|
// Just check that it didn't report no matches found
|
||||||
|
assert(!wildcard_result.contains("No repositories found"),
|
||||||
|
"Wildcard matching should find at least one repository");
|
||||||
|
|
||||||
|
print("\n===== All Git path matching tests completed! =====");
|
||||||
|
|
||||||
|
"Git path matching functionality works correctly"
|
124
rhaiexamples/git_test.rhai
Normal file
124
rhaiexamples/git_test.rhai
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// Git operations test script (primarily focused on validation)
|
||||||
|
// Note: Many git operations are destructive or require network access,
|
||||||
|
// so this test primarily validates availability and URL handling.
|
||||||
|
|
||||||
|
print("===== Git Operations Test =====");
|
||||||
|
|
||||||
|
// Test git availability
|
||||||
|
print("\n=== Test Git Availability ===");
|
||||||
|
let git_cmd = which("git");
|
||||||
|
if git_cmd {
|
||||||
|
print(`Git is available at: ${git_cmd}`);
|
||||||
|
} else {
|
||||||
|
print("WARNING: Git is not installed. Some tests will be skipped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test git URL parsing (testing internal implementation through git operations)
|
||||||
|
print("\n=== Test Git URL Parsing ===");
|
||||||
|
// HTTPS URLs
|
||||||
|
let https_urls = [
|
||||||
|
"https://github.com/user/repo.git",
|
||||||
|
"https://github.com/user/repo",
|
||||||
|
"https://example.com/user/repo.git"
|
||||||
|
];
|
||||||
|
|
||||||
|
print("Testing HTTPS GitHub URLs:");
|
||||||
|
for url in https_urls {
|
||||||
|
// For testing purposes, we'll use the URL rather than actually cloning
|
||||||
|
// Just check if git_clone responds with "already exists" message which indicates
|
||||||
|
// it recognized and parsed the URL correctly
|
||||||
|
let result = git_clone(url);
|
||||||
|
|
||||||
|
// Check if the result contains a path with expected structure
|
||||||
|
let contains_path = result.contains("/code/") &&
|
||||||
|
(result.contains("github.com") || result.contains("example.com"));
|
||||||
|
|
||||||
|
print(` URL: ${url}`);
|
||||||
|
print(` Path structure valid: ${contains_path ? "yes" : "no"}`);
|
||||||
|
|
||||||
|
// Extract the expected path components from the parsing
|
||||||
|
if contains_path {
|
||||||
|
let parts = result.split("/");
|
||||||
|
let domain_part = "";
|
||||||
|
let user_part = "";
|
||||||
|
let repo_part = "";
|
||||||
|
|
||||||
|
let found_code = false;
|
||||||
|
for i in 0..parts.len() {
|
||||||
|
if parts[i] == "code" && (i+3) < parts.len() {
|
||||||
|
domain_part = parts[i+1];
|
||||||
|
user_part = parts[i+2];
|
||||||
|
repo_part = parts[i+3];
|
||||||
|
found_code = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found_code {
|
||||||
|
print(` Parsed domain: ${domain_part}`);
|
||||||
|
print(` Parsed user: ${user_part}`);
|
||||||
|
print(` Parsed repo: ${repo_part}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH URLs
|
||||||
|
let ssh_urls = [
|
||||||
|
"git@github.com:user/repo.git",
|
||||||
|
"git@example.com:organization/repository.git"
|
||||||
|
];
|
||||||
|
|
||||||
|
print("\nTesting SSH Git URLs:");
|
||||||
|
for url in ssh_urls {
|
||||||
|
// Similar approach to HTTPS testing
|
||||||
|
let result = git_clone(url);
|
||||||
|
let contains_path = result.contains("/code/");
|
||||||
|
|
||||||
|
print(` URL: ${url}`);
|
||||||
|
print(` Path structure valid: ${contains_path ? "yes" : "no"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test git_list
|
||||||
|
print("\n=== Test git_list() ===");
|
||||||
|
let repos = git_list();
|
||||||
|
print(`Found ${repos.len()} local git repositories`);
|
||||||
|
if repos.len() > 0 {
|
||||||
|
print("First 3 repositories (or fewer if less available):");
|
||||||
|
let max = Math.min(3, repos.len());
|
||||||
|
for i in 0..max {
|
||||||
|
print(` ${i+1}. ${repos[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test git command access through run
|
||||||
|
print("\n=== Test Git Command Access ===");
|
||||||
|
if git_cmd {
|
||||||
|
let git_version = run("git --version");
|
||||||
|
print(`Git version info: ${git_version.stdout}`);
|
||||||
|
|
||||||
|
// Test git status command (this is safe and doesn't modify anything)
|
||||||
|
let git_status = run("git status");
|
||||||
|
print("Git status command:");
|
||||||
|
print(` Exit code: ${git_status.code}`);
|
||||||
|
if git_status.success {
|
||||||
|
print(" Git status executed successfully in current directory");
|
||||||
|
} else {
|
||||||
|
print(" Git status failed. This might not be a git repository.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Minimal testing of other git operations (these are mostly destructive)
|
||||||
|
print("\n=== Git Operations Availability Check ===");
|
||||||
|
print("The following git operations are available:");
|
||||||
|
print(" - git_clone: Available (tested above)");
|
||||||
|
print(" - git_list: Available (tested above)");
|
||||||
|
print(" - git_update: Available (not tested to avoid modifying repos)");
|
||||||
|
print(" - git_update_force: Available (not tested to avoid modifying repos)");
|
||||||
|
print(" - git_update_commit: Available (not tested to avoid modifying repos)");
|
||||||
|
print(" - git_update_commit_push: Available (not tested to avoid modifying repos)");
|
||||||
|
|
||||||
|
|
||||||
|
print("\nGit Operations Test completed!");
|
||||||
|
"Git Test Success"
|
@ -2,69 +2,6 @@
|
|||||||
|
|
||||||
The Buildah module provides functions for working with containers and images using the Buildah tool. Buildah helps you create and manage container images.
|
The Buildah module provides functions for working with containers and images using the Buildah tool. Buildah helps you create and manage container images.
|
||||||
|
|
||||||
## Builder Pattern
|
|
||||||
|
|
||||||
The Buildah module now supports a Builder pattern, which provides a more intuitive and flexible way to work with containers and images.
|
|
||||||
|
|
||||||
### Creating a Builder
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Create a builder with a name and base image
|
|
||||||
let builder = bah_new("my-container", "alpine:latest");
|
|
||||||
|
|
||||||
// Access builder properties
|
|
||||||
let container_id = builder.container_id;
|
|
||||||
let name = builder.name;
|
|
||||||
let image = builder.image;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Builder Methods
|
|
||||||
|
|
||||||
The Builder object provides the following methods:
|
|
||||||
|
|
||||||
- `run(command)`: Run a command in the container
|
|
||||||
- `run_with_isolation(command, isolation)`: Run a command with specified isolation
|
|
||||||
- `copy(source, dest)`: Copy files into the container
|
|
||||||
- `add(source, dest)`: Add files into the container
|
|
||||||
- `commit(image_name)`: Commit the container to an image
|
|
||||||
- `remove()`: Remove the container
|
|
||||||
- `reset()`: Remove the container and clear the container_id
|
|
||||||
- `config(options)`: Configure container metadata
|
|
||||||
- `images()`: List images in local storage
|
|
||||||
- `image_remove(image)`: Remove an image
|
|
||||||
- `image_pull(image, tls_verify)`: Pull an image from a registry
|
|
||||||
- `image_push(image, destination, tls_verify)`: Push an image to a registry
|
|
||||||
- `image_tag(image, new_name)`: Add a tag to an image
|
|
||||||
- `build(tag, context_dir, file, isolation)`: Build an image from a Dockerfile
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Create a builder
|
|
||||||
let builder = bah_new("my-container", "alpine:latest");
|
|
||||||
|
|
||||||
// Reset the builder to remove any existing container
|
|
||||||
builder.reset();
|
|
||||||
|
|
||||||
// Create a new container
|
|
||||||
builder = bah_new("my-container", "alpine:latest");
|
|
||||||
|
|
||||||
// Run a command
|
|
||||||
let result = builder.run("echo 'Hello from container'");
|
|
||||||
println(`Command output: ${result.stdout}`);
|
|
||||||
|
|
||||||
// Add a file
|
|
||||||
file_write("test_file.txt", "Test content");
|
|
||||||
builder.add("test_file.txt", "/");
|
|
||||||
|
|
||||||
// Commit to an image
|
|
||||||
builder.commit("my-custom-image:latest");
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
builder.remove();
|
|
||||||
delete("test_file.txt");
|
|
||||||
```
|
|
||||||
|
|
||||||
## Image Information
|
## Image Information
|
||||||
|
|
||||||
### Image Properties
|
### Image Properties
|
||||||
@ -77,42 +14,332 @@ When working with images, you can access the following information:
|
|||||||
- `size`: The size of the image
|
- `size`: The size of the image
|
||||||
- `created`: When the image was created
|
- `created`: When the image was created
|
||||||
|
|
||||||
## Builder Methods
|
## Container Functions
|
||||||
|
|
||||||
### `bah_new(name, image)`
|
### `bah_from(image)`
|
||||||
|
|
||||||
Creates a new Builder object for working with a container.
|
Creates a container from an image.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `name` (string): The name to give the container
|
|
||||||
- `image` (string): The name or ID of the image to create the container from
|
- `image` (string): The name or ID of the image to create the container from
|
||||||
|
|
||||||
**Returns:** A Builder object if successful.
|
**Returns:** The ID of the newly created container if successful.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
// Create a container from an image
|
||||||
|
let result = bah_from("alpine:latest");
|
||||||
|
let container_id = result.stdout;
|
||||||
|
print(`Created container: ${container_id}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_run(container, command)`
|
||||||
|
|
||||||
|
Runs a command in a container.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `container` (string): The container ID or name
|
||||||
|
- `command` (string): The command to run
|
||||||
|
|
||||||
|
**Returns:** The output of the command if successful.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```rhai
|
```rhai
|
||||||
// Create a new Builder
|
// Run a command in a container
|
||||||
let builder = bah_new("my-container", "alpine:latest");
|
let result = bah_run("my-container", "echo 'Hello from container'");
|
||||||
|
print(result.stdout);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Notes:**
|
### `bah_run_with_isolation(container, command, isolation)`
|
||||||
- If a container with the given name already exists, it will be reused instead of creating a new one
|
|
||||||
- The Builder object provides methods for working with the container
|
|
||||||
|
|
||||||
### `reset()`
|
Runs a command in a container with specified isolation.
|
||||||
|
|
||||||
Resets a Builder by removing the container and clearing the container_id. This allows you to start fresh with the same Builder object.
|
**Parameters:**
|
||||||
|
- `container` (string): The container ID or name
|
||||||
|
- `command` (string): The command to run
|
||||||
|
- `isolation` (string): The isolation type (e.g., "chroot", "rootless", "oci")
|
||||||
|
|
||||||
**Returns:** Nothing.
|
**Returns:** The output of the command if successful.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```rhai
|
```rhai
|
||||||
// Create a Builder
|
// Run a command with specific isolation
|
||||||
let builder = bah_new("my-container", "alpine:latest");
|
let result = bah_run_with_isolation("my-container", "ls -la", "chroot");
|
||||||
|
print(result.stdout);
|
||||||
// Reset the Builder to remove the container
|
```
|
||||||
builder.reset();
|
|
||||||
|
### `bah_copy(container, source, dest)`
|
||||||
// Create a new container with the same name
|
|
||||||
builder = bah_new("my-container", "alpine:latest");
|
Copies files into a container.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `container` (string): The container ID or name
|
||||||
|
- `source` (string): The source path on the host
|
||||||
|
- `dest` (string): The destination path in the container
|
||||||
|
|
||||||
|
**Returns:** A success message if the copy operation worked.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Copy a file into a container
|
||||||
|
bah_copy("my-container", "./app.js", "/app/app.js");
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_add(container, source, dest)`
|
||||||
|
|
||||||
|
Adds files into a container. Similar to `bah_copy` but can also handle remote URLs.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `container` (string): The container ID or name
|
||||||
|
- `source` (string): The source path on the host or a URL
|
||||||
|
- `dest` (string): The destination path in the container
|
||||||
|
|
||||||
|
**Returns:** A success message if the add operation worked.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Add a file from a URL into a container
|
||||||
|
bah_add("my-container", "https://example.com/file.tar.gz", "/app/");
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_commit(container, image_name)`
|
||||||
|
|
||||||
|
Commits a container to an image.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `container` (string): The container ID or name
|
||||||
|
- `image_name` (string): The name to give the new image
|
||||||
|
|
||||||
|
**Returns:** A success message if the commit operation worked.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Commit a container to an image
|
||||||
|
bah_commit("my-container", "my-image:latest");
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_remove(container)`
|
||||||
|
|
||||||
|
Removes a container.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `container` (string): The container ID or name
|
||||||
|
|
||||||
|
**Returns:** A success message if the container was removed.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Remove a container
|
||||||
|
bah_remove("my-container");
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_list()`
|
||||||
|
|
||||||
|
Lists containers.
|
||||||
|
|
||||||
|
**Returns:** A list of containers if successful.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// List containers
|
||||||
|
let result = bah_list();
|
||||||
|
print(result.stdout);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_new_build_options()`
|
||||||
|
|
||||||
|
Creates a new map with default build options.
|
||||||
|
|
||||||
|
**Returns:** A map with the following default options:
|
||||||
|
- `tag` (unit/null): The tag for the image (default: null)
|
||||||
|
- `context_dir` (string): The build context directory (default: ".")
|
||||||
|
- `file` (string): The Dockerfile path (default: "Dockerfile")
|
||||||
|
- `isolation` (unit/null): The isolation type (default: null)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Create build options
|
||||||
|
let options = bah_new_build_options();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_build(options)`
|
||||||
|
|
||||||
|
Builds an image with options specified in a map.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `options` (map): A map of options created with `bah_new_build_options()`
|
||||||
|
|
||||||
|
**Returns:** A success message if the build operation worked.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Create and customize build options
|
||||||
|
let options = bah_new_build_options();
|
||||||
|
options.tag = "my-image:latest";
|
||||||
|
options.context_dir = "./app";
|
||||||
|
options.file = "Dockerfile.prod";
|
||||||
|
options.isolation = "chroot";
|
||||||
|
|
||||||
|
// Build an image with options
|
||||||
|
let result = bah_build(options);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Functions
|
||||||
|
|
||||||
|
### `bah_images()`
|
||||||
|
|
||||||
|
Lists images in local storage.
|
||||||
|
|
||||||
|
**Returns:** A list of images if successful.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// List images
|
||||||
|
let images = bah_images();
|
||||||
|
|
||||||
|
// Display image information
|
||||||
|
for image in images {
|
||||||
|
print(`ID: ${image.id}, Name: ${image.name}, Size: ${image.size}, Created: ${image.created}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### `bah_image_remove(image)`
|
||||||
|
|
||||||
|
Removes one or more images.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `image` (string): The image ID or name
|
||||||
|
|
||||||
|
**Returns:** A success message if the image was removed.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Remove an image
|
||||||
|
bah_image_remove("my-image:latest");
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_image_push(image, destination, tls_verify)`
|
||||||
|
|
||||||
|
Pushes an image to a registry.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `image` (string): The image ID or name
|
||||||
|
- `destination` (string): The destination registry/repository
|
||||||
|
- `tls_verify` (boolean): Whether to verify TLS certificates
|
||||||
|
|
||||||
|
**Returns:** A success message if the image was pushed.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Push an image to a registry
|
||||||
|
bah_image_push("my-image:latest", "registry.example.com/my-repo/my-image:latest", true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_image_tag(image, new_name)`
|
||||||
|
|
||||||
|
Adds an additional name to a local image.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `image` (string): The image ID or name
|
||||||
|
- `new_name` (string): The new name to add
|
||||||
|
|
||||||
|
**Returns:** A success message if the image was tagged.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Tag an image with a new name
|
||||||
|
bah_image_tag("my-image:latest", "my-image:v1.0");
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_image_pull(image, tls_verify)`
|
||||||
|
|
||||||
|
Pulls an image from a registry.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `image` (string): The image to pull
|
||||||
|
- `tls_verify` (boolean): Whether to verify TLS certificates
|
||||||
|
|
||||||
|
**Returns:** A success message if the image was pulled.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Pull an image from a registry
|
||||||
|
bah_image_pull("alpine:latest", true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_new_commit_options()`
|
||||||
|
|
||||||
|
Creates a new map with default commit options.
|
||||||
|
|
||||||
|
**Returns:** A map with the following default options:
|
||||||
|
- `format` (unit/null): The format of the image (default: null)
|
||||||
|
- `squash` (boolean): Whether to squash layers (default: false)
|
||||||
|
- `rm` (boolean): Whether to remove the container after commit (default: false)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Create commit options
|
||||||
|
let options = bah_new_commit_options();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_image_commit(container, image_name, options)`
|
||||||
|
|
||||||
|
Commits a container to an image with options specified in a map.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `container` (string): The container ID or name
|
||||||
|
- `image_name` (string): The name to give the new image
|
||||||
|
- `options` (map): A map of options created with `bah_new_commit_options()`
|
||||||
|
|
||||||
|
**Returns:** A success message if the image was created.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Create and customize commit options
|
||||||
|
let options = bah_new_commit_options();
|
||||||
|
options.format = "docker";
|
||||||
|
options.squash = true;
|
||||||
|
options.rm = true;
|
||||||
|
|
||||||
|
// Commit a container to an image with options
|
||||||
|
let result = bah_image_commit("my-container", "my-image:latest", options);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_new_config_options()`
|
||||||
|
|
||||||
|
Creates a new map for config options.
|
||||||
|
|
||||||
|
**Returns:** An empty map to be filled with configuration options.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Create config options
|
||||||
|
let options = bah_new_config_options();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bah_config(container, options)`
|
||||||
|
|
||||||
|
Configures a container with options specified in a map.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `container` (string): The container ID or name
|
||||||
|
- `options` (map): A map of options created with `bah_new_config_options()`
|
||||||
|
|
||||||
|
**Returns:** A success message if the container was configured.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rhai
|
||||||
|
// Create and customize config options
|
||||||
|
let options = bah_new_config_options();
|
||||||
|
options.author = "John Doe";
|
||||||
|
options.cmd = "echo Hello";
|
||||||
|
options.entrypoint = "/bin/sh -c";
|
||||||
|
options.workingdir = "/app";
|
||||||
|
options.env = "NODE_ENV=production";
|
||||||
|
options.label = "version=1.0";
|
||||||
|
|
||||||
|
// Configure a container with options
|
||||||
|
let result = bah_config("my-container", options);
|
||||||
```
|
```
|
||||||
|
@ -2,203 +2,112 @@
|
|||||||
|
|
||||||
This module provides Rhai wrappers for the Git functionality in SAL.
|
This module provides Rhai wrappers for the Git functionality in SAL.
|
||||||
|
|
||||||
> **Note:** The constructor for GitTree has been renamed from `new()` to `gittree_new()` to avoid confusion with other constructors. This makes the interface more explicit and less likely to cause naming conflicts.
|
## Basic Git Operations
|
||||||
|
|
||||||
## Object-Oriented Design
|
### Clone a Repository
|
||||||
|
|
||||||
The Git module follows an object-oriented design with two main classes:
|
|
||||||
|
|
||||||
1. **GitTree** - Represents a collection of git repositories under a base path
|
|
||||||
- Created with `gittree_new(base_path)`
|
|
||||||
- Methods for listing, finding, and getting repositories
|
|
||||||
|
|
||||||
2. **GitRepo** - Represents a single git repository
|
|
||||||
- Obtained from GitTree's `get()` method
|
|
||||||
- Methods for common git operations: pull, reset, push, commit
|
|
||||||
|
|
||||||
This design allows for a more intuitive and flexible interface, with method chaining for complex operations.
|
|
||||||
|
|
||||||
## Creating a GitTree
|
|
||||||
|
|
||||||
The GitTree object is the main entry point for git operations. It represents a collection of git repositories under a base path.
|
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// Create a new GitTree with a base path
|
// Clone a repository to a standardized location in the user's home directory
|
||||||
let git_tree = gittree_new("/root/code");
|
let repo_path = git_clone("https://github.com/username/repo.git");
|
||||||
print(`Created GitTree with base path: /home/user/code`);
|
print(`Repository cloned to: ${repo_path}`);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Finding Repositories
|
### List Repositories
|
||||||
|
|
||||||
### List All Repositories
|
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// List all git repositories under the base path
|
// List all git repositories in the user's ~/code directory
|
||||||
let repos = git_tree.list();
|
let repos = git_list();
|
||||||
print(`Found ${repos.len()} repositories`);
|
print("Found repositories:");
|
||||||
|
|
||||||
// Print the repositories
|
|
||||||
for repo in repos {
|
for repo in repos {
|
||||||
print(` - ${repo}`);
|
print(` - ${repo}`);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Find Repositories Matching a Pattern
|
### Find Repositories
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// Find repositories matching a pattern
|
// Find repositories matching a pattern
|
||||||
// Use a wildcard (*) suffix to find multiple matches
|
// Use a wildcard (*) suffix to find multiple matches
|
||||||
let matching_repos = git_tree.find("my-project*");
|
let matching_repos = find_matching_repos("my-project*");
|
||||||
print("Matching repositories:");
|
print("Matching repositories:");
|
||||||
for repo in matching_repos {
|
for repo in matching_repos {
|
||||||
print(` - ${repo}`);
|
print(` - ${repo}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a specific repository (must match exactly one)
|
// Find a specific repository (must match exactly one)
|
||||||
let specific_repo = git_tree.find("unique-project")[0];
|
let specific_repo = find_matching_repos("unique-project")[0];
|
||||||
print(`Found specific repository: ${specific_repo}`);
|
print(`Found specific repository: ${specific_repo}`);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Working with Repositories
|
|
||||||
|
|
||||||
### Get Repository Objects
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Get GitRepo objects for repositories matching a pattern
|
|
||||||
let repos = git_tree.get("my-project*");
|
|
||||||
print(`Found ${repos.len()} repositories`);
|
|
||||||
|
|
||||||
// Get a specific repository
|
|
||||||
let repo = git_tree.get("unique-project")[0];
|
|
||||||
print(`Working with repository: ${repo.path()}`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clone a Repository
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Clone a repository by URL
|
|
||||||
// This will clone the repository to the base path of the GitTree
|
|
||||||
let repos = git_tree.get("https://github.com/username/repo.git");
|
|
||||||
let repo = repos[0];
|
|
||||||
print(`Repository cloned to: ${repo.path()}`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check for Changes
|
### Check for Changes
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// Check if a repository has uncommitted changes
|
// Check if a repository has uncommitted changes
|
||||||
let repo = git_tree.get("my-project")[0];
|
let repo_path = "/path/to/repo";
|
||||||
if repo.has_changes() {
|
if git_has_changes(repo_path) {
|
||||||
print("Repository has uncommitted changes");
|
print("Repository has uncommitted changes");
|
||||||
} else {
|
} else {
|
||||||
print("Repository is clean");
|
print("Repository is clean");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Repository Operations
|
## Repository Updates
|
||||||
|
|
||||||
### Pull Changes
|
### Update a Repository
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// Pull the latest changes from the remote
|
// Update a repository by pulling the latest changes
|
||||||
// This will fail if there are uncommitted changes
|
// This will fail if there are uncommitted changes
|
||||||
let repo = git_tree.get("my-project")[0];
|
let result = git_update("my-project");
|
||||||
let result = repo.pull();
|
print(result);
|
||||||
print("Repository updated successfully");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reset Local Changes
|
### Force Update a Repository
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// Reset any local changes in the repository
|
// Force update a repository by discarding local changes and pulling the latest changes
|
||||||
let repo = git_tree.get("my-project")[0];
|
let result = git_update_force("my-project");
|
||||||
let result = repo.reset();
|
print(result);
|
||||||
print("Repository reset successfully");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Commit Changes
|
### Commit and Update
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// Commit changes in the repository
|
// Commit changes in a repository and then update it by pulling the latest changes
|
||||||
let repo = git_tree.get("my-project")[0];
|
let result = git_update_commit("my-project", "Fix bug in login form");
|
||||||
let result = repo.commit("Fix bug in login form");
|
print(result);
|
||||||
print("Changes committed successfully");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Push Changes
|
### Commit and Push
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// Push changes to the remote
|
// Commit changes in a repository and push them to the remote
|
||||||
let repo = git_tree.get("my-project")[0];
|
let result = git_update_commit_push("my-project", "Add new feature");
|
||||||
let result = repo.push();
|
print(result);
|
||||||
print("Changes pushed successfully");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Method Chaining
|
|
||||||
|
|
||||||
The GitRepo methods can be chained together for more complex operations:
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Commit changes and push them to the remote
|
|
||||||
let repo = git_tree.get("my-project")[0];
|
|
||||||
let result = repo.commit("Add new feature").push();
|
|
||||||
print("Changes committed and pushed successfully");
|
|
||||||
|
|
||||||
// Reset local changes, pull the latest changes, and commit new changes
|
|
||||||
let repo = git_tree.get("my-project")[0];
|
|
||||||
let result = repo.reset().pull().commit("Update dependencies");
|
|
||||||
print("Repository updated successfully");
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Example
|
## Complete Example
|
||||||
|
|
||||||
```rhai
|
```rhai
|
||||||
// Create a new GitTree
|
|
||||||
let home_dir = env("HOME");
|
|
||||||
let git_tree = gittree_new(`${home_dir}/code`);
|
|
||||||
|
|
||||||
// Clone a repository
|
// Clone a repository
|
||||||
let repos = git_tree.get("https://github.com/username/example-repo.git");
|
let repo_url = "https://github.com/username/example-repo.git";
|
||||||
let repo = repos[0];
|
let repo_path = git_clone(repo_url);
|
||||||
print(`Cloned repository to: ${repo.path()}`);
|
print(`Cloned repository to: ${repo_path}`);
|
||||||
|
|
||||||
// Make some changes (using OS module functions)
|
// Make some changes (using OS module functions)
|
||||||
let file_path = `${repo.path()}/README.md`;
|
let file_path = `${repo_path}/README.md`;
|
||||||
let content = "# Example Repository\n\nThis is an example repository.";
|
let content = "# Example Repository\n\nThis is an example repository.";
|
||||||
write_file(file_path, content);
|
write_file(file_path, content);
|
||||||
|
|
||||||
// Commit and push the changes
|
// Commit and push the changes
|
||||||
let result = repo.commit("Update README.md").push();
|
let commit_message = "Update README.md";
|
||||||
print("Changes committed and pushed successfully");
|
let result = git_update_commit_push(repo_path, commit_message);
|
||||||
|
print(result);
|
||||||
|
|
||||||
// List all repositories
|
// List all repositories
|
||||||
let all_repos = git_tree.list();
|
let repos = git_list();
|
||||||
print("All repositories:");
|
print("All repositories:");
|
||||||
for repo_path in all_repos {
|
for repo in repos {
|
||||||
print(` - ${repo_path}`);
|
print(` - ${repo}`);
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
All methods in the Git module return a Result type, which means they can either succeed or fail with an error. If an error occurs, it will be propagated to the Rhai script as a runtime error.
|
|
||||||
|
|
||||||
For example, if you try to clone a repository that doesn't exist:
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Try to clone a non-existent repository
|
|
||||||
try {
|
|
||||||
let git_tree = gittree_new("/root/code");
|
|
||||||
let repos = git_tree.get("https://github.com/nonexistent/repo.git");
|
|
||||||
print("This will not be executed if the repository doesn't exist");
|
|
||||||
} catch(err) {
|
|
||||||
print(`Error: ${err}`); // Will print the error message from git
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Common errors include:
|
|
||||||
- Invalid URL
|
|
||||||
- Repository not found
|
|
||||||
- Authentication failure
|
|
||||||
- Network issues
|
|
||||||
- Local changes exist when trying to pull
|
|
||||||
|
66
src/examples/git_test.rs
Normal file
66
src/examples/git_test.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
//! Example of using the Git module with Rhai
|
||||||
|
//!
|
||||||
|
//! This example demonstrates how to use the Git module functions
|
||||||
|
//! through the Rhai scripting language.
|
||||||
|
|
||||||
|
use sal::rhai::{self, Engine};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Create a new Rhai engine
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
// Register SAL functions with the engine
|
||||||
|
rhai::register(&mut engine)?;
|
||||||
|
|
||||||
|
// Run a Rhai script that uses Git functions
|
||||||
|
let script = r#"
|
||||||
|
// Print a header
|
||||||
|
print("=== Testing Git Module Functions ===\n");
|
||||||
|
|
||||||
|
// Test git_list function
|
||||||
|
print("Listing git repositories...");
|
||||||
|
let repos = git_list();
|
||||||
|
print(`Found ${repos.len()} repositories`);
|
||||||
|
|
||||||
|
// Print the first few repositories
|
||||||
|
if repos.len() > 0 {
|
||||||
|
print("First few repositories:");
|
||||||
|
let count = if repos.len() > 3 { 3 } else { repos.len() };
|
||||||
|
for i in range(0, count) {
|
||||||
|
print(` - ${repos[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test find_matching_repos function
|
||||||
|
if repos.len() > 0 {
|
||||||
|
print("\nTesting repository search...");
|
||||||
|
// Extract a part of the first repo name to search for
|
||||||
|
let repo_path = repos[0];
|
||||||
|
let parts = repo_path.split("/");
|
||||||
|
let repo_name = parts[parts.len() - 1];
|
||||||
|
|
||||||
|
print(`Searching for repositories containing "${repo_name}"`);
|
||||||
|
let matching = find_matching_repos(repo_name);
|
||||||
|
|
||||||
|
print(`Found ${matching.len()} matching repositories`);
|
||||||
|
for repo in matching {
|
||||||
|
print(` - ${repo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a repository has changes
|
||||||
|
print("\nChecking for changes in repository...");
|
||||||
|
let has_changes = git_has_changes(repo_path);
|
||||||
|
print(`Repository ${repo_path} has changes: ${has_changes}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n=== Git Module Test Complete ===");
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// Evaluate the script
|
||||||
|
match engine.eval::<()>(script) {
|
||||||
|
Ok(_) => println!("Script executed successfully"),
|
||||||
|
Err(e) => eprintln!("Script execution error: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
66
src/examples/rhai_git_example.rs
Normal file
66
src/examples/rhai_git_example.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
//! Example of using the Git module with Rhai
|
||||||
|
//!
|
||||||
|
//! This example demonstrates how to use the Git module functions
|
||||||
|
//! through the Rhai scripting language.
|
||||||
|
|
||||||
|
use sal::rhai::{self, Engine};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Create a new Rhai engine
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
// Register SAL functions with the engine
|
||||||
|
rhai::register(&mut engine)?;
|
||||||
|
|
||||||
|
// Run a Rhai script that uses Git functions
|
||||||
|
let script = r#"
|
||||||
|
// Print a header
|
||||||
|
print("=== Testing Git Module Functions ===\n");
|
||||||
|
|
||||||
|
// Test git_list function
|
||||||
|
print("Listing git repositories...");
|
||||||
|
let repos = git_list();
|
||||||
|
print(`Found ${repos.len()} repositories`);
|
||||||
|
|
||||||
|
// Print the first few repositories
|
||||||
|
if repos.len() > 0 {
|
||||||
|
print("First few repositories:");
|
||||||
|
let count = if repos.len() > 3 { 3 } else { repos.len() };
|
||||||
|
for i in range(0, count) {
|
||||||
|
print(` - ${repos[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test find_matching_repos function
|
||||||
|
if repos.len() > 0 {
|
||||||
|
print("\nTesting repository search...");
|
||||||
|
// Extract a part of the first repo name to search for
|
||||||
|
let repo_path = repos[0];
|
||||||
|
let parts = repo_path.split("/");
|
||||||
|
let repo_name = parts[parts.len() - 1];
|
||||||
|
|
||||||
|
print(`Searching for repositories containing "${repo_name}"`);
|
||||||
|
let matching = find_matching_repos(repo_name);
|
||||||
|
|
||||||
|
print(`Found ${matching.len()} matching repositories`);
|
||||||
|
for repo in matching {
|
||||||
|
print(` - ${repo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a repository has changes
|
||||||
|
print("\nChecking for changes in repository...");
|
||||||
|
let has_changes = git_has_changes(repo_path);
|
||||||
|
print(`Repository ${repo_path} has changes: ${has_changes}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n=== Git Module Test Complete ===");
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// Evaluate the script
|
||||||
|
match engine.eval::<()>(script) {
|
||||||
|
Ok(_) => println!("Script executed successfully"),
|
||||||
|
Err(e) => eprintln!("Script execution error: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
850
src/git/git.rs
850
src/git/git.rs
@ -11,7 +11,6 @@ use std::error::Error;
|
|||||||
pub enum GitError {
|
pub enum GitError {
|
||||||
GitNotInstalled(std::io::Error),
|
GitNotInstalled(std::io::Error),
|
||||||
InvalidUrl(String),
|
InvalidUrl(String),
|
||||||
InvalidBasePath(String),
|
|
||||||
HomeDirectoryNotFound(std::env::VarError),
|
HomeDirectoryNotFound(std::env::VarError),
|
||||||
FileSystemError(std::io::Error),
|
FileSystemError(std::io::Error),
|
||||||
GitCommandFailed(String),
|
GitCommandFailed(String),
|
||||||
@ -29,7 +28,6 @@ impl fmt::Display for GitError {
|
|||||||
match self {
|
match self {
|
||||||
GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e),
|
GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e),
|
||||||
GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url),
|
GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url),
|
||||||
GitError::InvalidBasePath(path) => write!(f, "Invalid base path: {}", path),
|
|
||||||
GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e),
|
GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e),
|
||||||
GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e),
|
GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e),
|
||||||
GitError::GitCommandFailed(e) => write!(f, "{}", e),
|
GitError::GitCommandFailed(e) => write!(f, "{}", e),
|
||||||
@ -57,21 +55,98 @@ impl Error for GitError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a git URL to extract the server, account, and repository name.
|
// Git utility functions
|
||||||
///
|
|
||||||
/// # Arguments
|
/**
|
||||||
///
|
* Clones a git repository to a standardized location in the user's home directory.
|
||||||
/// * `url` - The URL of the git repository to parse. Can be in HTTPS format
|
*
|
||||||
/// (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
* # Arguments
|
||||||
///
|
*
|
||||||
/// # Returns
|
* * `url` - The URL of the git repository to clone. Can be in HTTPS format
|
||||||
///
|
* (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
||||||
/// A tuple containing:
|
*
|
||||||
/// * `server` - The server name (e.g., "github.com")
|
* # Returns
|
||||||
/// * `account` - The account or organization name (e.g., "username")
|
*
|
||||||
/// * `repo` - The repository name (e.g., "repo")
|
* * `Ok(String)` - The path where the repository was cloned, formatted as
|
||||||
///
|
* ~/code/server/account/repo (e.g., ~/code/github.com/username/repo).
|
||||||
/// If the URL cannot be parsed, all three values will be empty strings.
|
* * `Err(GitError)` - An error if the clone operation failed.
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let repo_path = git_clone("https://github.com/username/repo.git")?;
|
||||||
|
* println!("Repository cloned to: {}", repo_path);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_clone(url: &str) -> Result<String, GitError> {
|
||||||
|
// Check if git is installed
|
||||||
|
let _git_check = Command::new("git")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::GitNotInstalled)?;
|
||||||
|
|
||||||
|
// Parse the URL to determine the clone path
|
||||||
|
let (server, account, repo) = parse_git_url(url);
|
||||||
|
if server.is_empty() || account.is_empty() || repo.is_empty() {
|
||||||
|
return Err(GitError::InvalidUrl(url.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the target directory
|
||||||
|
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
|
||||||
|
|
||||||
|
let clone_path = format!("{}/code/{}/{}/{}", home_dir, server, account, repo);
|
||||||
|
let clone_dir = Path::new(&clone_path);
|
||||||
|
|
||||||
|
// Check if repo already exists
|
||||||
|
if clone_dir.exists() {
|
||||||
|
return Ok(format!("Repository already exists at {}", clone_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create parent directory
|
||||||
|
if let Some(parent) = clone_dir.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the repository
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(&["clone", "--depth", "1", url, &clone_path])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(clone_path)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a git URL to extract the server, account, and repository name.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `url` - The URL of the git repository to parse. Can be in HTTPS format
|
||||||
|
* (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* A tuple containing:
|
||||||
|
* * `server` - The server name (e.g., "github.com")
|
||||||
|
* * `account` - The account or organization name (e.g., "username")
|
||||||
|
* * `repo` - The repository name (e.g., "repo")
|
||||||
|
*
|
||||||
|
* If the URL cannot be parsed, all three values will be empty strings.
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let (server, account, repo) = parse_git_url("https://github.com/username/repo.git");
|
||||||
|
* assert_eq!(server, "github.com");
|
||||||
|
* assert_eq!(account, "username");
|
||||||
|
* assert_eq!(repo, "repo");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
pub fn parse_git_url(url: &str) -> (String, String, String) {
|
pub fn parse_git_url(url: &str) -> (String, String, String) {
|
||||||
// HTTP(S) URL format: https://github.com/username/repo.git
|
// HTTP(S) URL format: https://github.com/username/repo.git
|
||||||
let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
|
let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
|
||||||
@ -96,394 +171,427 @@ pub fn parse_git_url(url: &str) -> (String, String, String) {
|
|||||||
(String::new(), String::new(), String::new())
|
(String::new(), String::new(), String::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if git is installed on the system.
|
/**
|
||||||
///
|
* Lists all git repositories found in the user's ~/code directory.
|
||||||
/// # Returns
|
*
|
||||||
///
|
* This function searches for directories containing a .git subdirectory,
|
||||||
/// * `Ok(())` - If git is installed
|
* which indicates a git repository.
|
||||||
/// * `Err(GitError)` - If git is not installed
|
*
|
||||||
fn check_git_installed() -> Result<(), GitError> {
|
* # Returns
|
||||||
Command::new("git")
|
*
|
||||||
.arg("--version")
|
* * `Ok(Vec<String>)` - A vector of paths to git repositories
|
||||||
.output()
|
* * `Err(GitError)` - An error if the operation failed
|
||||||
.map_err(GitError::GitNotInstalled)?;
|
*
|
||||||
Ok(())
|
* # Examples
|
||||||
}
|
*
|
||||||
|
* ```
|
||||||
|
* let repos = git_list()?;
|
||||||
|
* for repo in repos {
|
||||||
|
* println!("Found repository: {}", repo);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_list() -> Result<Vec<String>, GitError> {
|
||||||
|
// Get home directory
|
||||||
|
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
|
||||||
|
|
||||||
/// Represents a collection of git repositories under a base path.
|
let code_dir = format!("{}/code", home_dir);
|
||||||
#[derive(Clone)]
|
let code_path = Path::new(&code_dir);
|
||||||
pub struct GitTree {
|
|
||||||
base_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GitTree {
|
if !code_path.exists() || !code_path.is_dir() {
|
||||||
/// Creates a new GitTree with the specified base path.
|
return Ok(Vec::new());
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `base_path` - The base path where all git repositories are located
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(GitTree)` - A new GitTree instance
|
|
||||||
/// * `Err(GitError)` - If the base path is invalid or cannot be created
|
|
||||||
pub fn new(base_path: &str) -> Result<Self, GitError> {
|
|
||||||
// Check if git is installed
|
|
||||||
check_git_installed()?;
|
|
||||||
|
|
||||||
// Validate the base path
|
|
||||||
let path = Path::new(base_path);
|
|
||||||
if !path.exists() {
|
|
||||||
fs::create_dir_all(path).map_err(|e| {
|
|
||||||
GitError::FileSystemError(e)
|
|
||||||
})?;
|
|
||||||
} else if !path.is_dir() {
|
|
||||||
return Err(GitError::InvalidBasePath(base_path.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(GitTree {
|
|
||||||
base_path: base_path.to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lists all git repositories under the base path.
|
let mut repos = Vec::new();
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(Vec<String>)` - A vector of paths to git repositories
|
|
||||||
/// * `Err(GitError)` - If the operation failed
|
|
||||||
pub fn list(&self) -> Result<Vec<String>, GitError> {
|
|
||||||
let base_path = Path::new(&self.base_path);
|
|
||||||
|
|
||||||
if !base_path.exists() || !base_path.is_dir() {
|
// Find all directories with .git subdirectories
|
||||||
return Ok(Vec::new());
|
let output = Command::new("find")
|
||||||
}
|
.args(&[&code_dir, "-type", "d", "-name", ".git"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
let mut repos = Vec::new();
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
// Find all directories with .git subdirectories
|
for line in stdout.lines() {
|
||||||
let output = Command::new("find")
|
// Get the parent directory of .git which is the repo root
|
||||||
.args(&[&self.base_path, "-type", "d", "-name", ".git"])
|
if let Some(parent) = Path::new(line).parent() {
|
||||||
.output()
|
if let Some(path_str) = parent.to_str() {
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
repos.push(path_str.to_string());
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
for line in stdout.lines() {
|
|
||||||
// Get the parent directory of .git which is the repo root
|
|
||||||
if let Some(parent) = Path::new(line).parent() {
|
|
||||||
if let Some(path_str) = parent.to_str() {
|
|
||||||
repos.push(path_str.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let error = String::from_utf8_lossy(&output.stderr);
|
|
||||||
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
Ok(repos)
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds repositories matching a pattern or partial path.
|
Ok(repos)
|
||||||
///
|
}
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `pattern` - The pattern to match against repository paths
|
|
||||||
/// - If the pattern ends with '*', all matching repositories are returned
|
|
||||||
/// - Otherwise, exactly one matching repository must be found
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(Vec<String>)` - A vector of paths to matching repositories
|
|
||||||
/// * `Err(GitError)` - If no matching repositories are found,
|
|
||||||
/// or if multiple repositories match a non-wildcard pattern
|
|
||||||
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> {
|
|
||||||
// Get all repos
|
|
||||||
let repos = self.list()?;
|
|
||||||
|
|
||||||
if repos.is_empty() {
|
/**
|
||||||
return Err(GitError::NoRepositoriesFound);
|
* Checks if a git repository has uncommitted changes.
|
||||||
}
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
|
||||||
|
* * `Err(GitError)` - An error if the operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* if has_git_changes("/path/to/repo")? {
|
||||||
|
* println!("Repository has uncommitted changes");
|
||||||
|
* } else {
|
||||||
|
* println!("Repository is clean");
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn has_git_changes(repo_path: &str) -> Result<bool, GitError> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(&["-C", repo_path, "status", "--porcelain"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
// Check if pattern ends with wildcard
|
Ok(!output.stdout.is_empty())
|
||||||
if pattern.ends_with('*') {
|
}
|
||||||
let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
|
|
||||||
let matching: Vec<String> = repos.iter()
|
|
||||||
.filter(|repo| repo.contains(search_pattern))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if matching.is_empty() {
|
/**
|
||||||
return Err(GitError::RepositoryNotFound(pattern.to_string()));
|
* Finds repositories matching a pattern or partial path.
|
||||||
}
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `pattern` - The pattern to match against repository paths
|
||||||
|
* - If the pattern ends with '*', all matching repositories are returned
|
||||||
|
* - Otherwise, exactly one matching repository must be found
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(Vec<String>)` - A vector of paths to matching repositories
|
||||||
|
* * `Err(GitError)` - An error if no matching repositories are found,
|
||||||
|
* or if multiple repositories match a non-wildcard pattern
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Find all repositories containing "project"
|
||||||
|
* let repos = find_matching_repos("project*")?;
|
||||||
|
*
|
||||||
|
* // Find exactly one repository containing "unique-project"
|
||||||
|
* let repo = find_matching_repos("unique-project")?[0];
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn find_matching_repos(pattern: &str) -> Result<Vec<String>, GitError> {
|
||||||
|
// Get all repos
|
||||||
|
let repos = git_list()?;
|
||||||
|
|
||||||
Ok(matching)
|
if repos.is_empty() {
|
||||||
} else {
|
return Err(GitError::NoRepositoriesFound);
|
||||||
// No wildcard, need to find exactly one match
|
|
||||||
let matching: Vec<String> = repos.iter()
|
|
||||||
.filter(|repo| repo.contains(pattern))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
match matching.len() {
|
|
||||||
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
|
|
||||||
1 => Ok(matching),
|
|
||||||
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets one or more GitRepo objects based on a path pattern or URL.
|
// Check if pattern ends with wildcard
|
||||||
///
|
if pattern.ends_with('*') {
|
||||||
/// # Arguments
|
let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
|
||||||
///
|
let matching: Vec<String> = repos.iter()
|
||||||
/// * `path_or_url` - The path pattern to match against repository paths or a git URL
|
.filter(|repo| repo.contains(search_pattern))
|
||||||
/// - If it's a URL, the repository will be cloned if it doesn't exist
|
.cloned()
|
||||||
/// - If it's a path pattern, it will find matching repositories
|
.collect();
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(Vec<GitRepo>)` - A vector of GitRepo objects
|
|
||||||
/// * `Err(GitError)` - If no matching repositories are found or the clone operation failed
|
|
||||||
pub fn get(&self, path_or_url: &str) -> Result<Vec<GitRepo>, GitError> {
|
|
||||||
// Check if it's a URL
|
|
||||||
if path_or_url.starts_with("http") || path_or_url.starts_with("git@") {
|
|
||||||
// Parse the URL
|
|
||||||
let (server, account, repo) = parse_git_url(path_or_url);
|
|
||||||
if server.is_empty() || account.is_empty() || repo.is_empty() {
|
|
||||||
return Err(GitError::InvalidUrl(path_or_url.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the target directory
|
if matching.is_empty() {
|
||||||
let clone_path = format!("{}/{}/{}/{}", self.base_path, server, account, repo);
|
return Err(GitError::RepositoryNotFound(pattern.to_string()));
|
||||||
let clone_dir = Path::new(&clone_path);
|
}
|
||||||
|
|
||||||
// Check if repo already exists
|
Ok(matching)
|
||||||
if clone_dir.exists() {
|
} else {
|
||||||
return Ok(vec![GitRepo::new(clone_path)]);
|
// No wildcard, need to find exactly one match
|
||||||
}
|
let matching: Vec<String> = repos.iter()
|
||||||
|
.filter(|repo| repo.contains(pattern))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Create parent directory
|
match matching.len() {
|
||||||
if let Some(parent) = clone_dir.parent() {
|
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
|
||||||
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?;
|
1 => Ok(matching),
|
||||||
}
|
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
|
||||||
|
|
||||||
// Clone the repository
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args(&["clone", "--depth", "1", path_or_url, &clone_path])
|
|
||||||
.output()
|
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
Ok(vec![GitRepo::new(clone_path)])
|
|
||||||
} else {
|
|
||||||
let error = String::from_utf8_lossy(&output.stderr);
|
|
||||||
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It's a path pattern, find matching repositories
|
|
||||||
let repo_paths = self.find(path_or_url)?;
|
|
||||||
|
|
||||||
// Convert paths to GitRepo objects
|
|
||||||
let repos: Vec<GitRepo> = repo_paths.into_iter()
|
|
||||||
.map(GitRepo::new)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(repos)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a git repository.
|
/**
|
||||||
pub struct GitRepo {
|
* Updates a git repository by pulling the latest changes.
|
||||||
path: String,
|
*
|
||||||
}
|
* This function will fail if there are uncommitted changes in the repository.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating the repository was updated
|
||||||
|
* * `Err(GitError)` - An error if the update failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = git_update("my-project")?;
|
||||||
|
* println!("{}", result); // "Successfully updated repository at /home/user/code/github.com/user/my-project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_update(repo_path: &str) -> Result<String, GitError> {
|
||||||
|
// If repo_path may be a partial path, find the matching repository
|
||||||
|
let repos = find_matching_repos(repo_path)?;
|
||||||
|
|
||||||
impl GitRepo {
|
// Should only be one repository at this point
|
||||||
/// Creates a new GitRepo with the specified path.
|
let actual_path = &repos[0];
|
||||||
///
|
|
||||||
/// # Arguments
|
// Check if repository exists and is a git repository
|
||||||
///
|
let git_dir = Path::new(actual_path).join(".git");
|
||||||
/// * `path` - The path to the git repository
|
if !git_dir.exists() || !git_dir.is_dir() {
|
||||||
pub fn new(path: String) -> Self {
|
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||||
GitRepo { path }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the path of the repository.
|
// Check for local changes
|
||||||
///
|
if has_git_changes(actual_path)? {
|
||||||
/// # Returns
|
return Err(GitError::LocalChangesExist(actual_path.clone()));
|
||||||
///
|
|
||||||
/// * The path to the git repository
|
|
||||||
pub fn path(&self) -> &str {
|
|
||||||
&self.path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the repository has uncommitted changes.
|
// Pull the latest changes
|
||||||
///
|
let output = Command::new("git")
|
||||||
/// # Returns
|
.args(&["-C", actual_path, "pull"])
|
||||||
///
|
.output()
|
||||||
/// * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
/// * `Err(GitError)` - If the operation failed
|
|
||||||
pub fn has_changes(&self) -> Result<bool, GitError> {
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args(&["-C", &self.path, "status", "--porcelain"])
|
|
||||||
.output()
|
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
|
||||||
|
|
||||||
Ok(!output.stdout.is_empty())
|
if output.status.success() {
|
||||||
}
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if stdout.contains("Already up to date") {
|
||||||
/// Pulls the latest changes from the remote repository.
|
Ok(format!("Repository already up to date at {}", actual_path))
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
|
||||||
/// * `Err(GitError)` - If the pull operation failed
|
|
||||||
pub fn pull(&self) -> Result<Self, GitError> {
|
|
||||||
// Check if repository exists and is a git repository
|
|
||||||
let git_dir = Path::new(&self.path).join(".git");
|
|
||||||
if !git_dir.exists() || !git_dir.is_dir() {
|
|
||||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for local changes
|
|
||||||
if self.has_changes()? {
|
|
||||||
return Err(GitError::LocalChangesExist(self.path.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pull the latest changes
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args(&["-C", &self.path, "pull"])
|
|
||||||
.output()
|
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
Ok(self.clone())
|
|
||||||
} else {
|
} else {
|
||||||
let error = String::from_utf8_lossy(&output.stderr);
|
Ok(format!("Successfully updated repository at {}", actual_path))
|
||||||
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets any local changes in the repository.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
|
||||||
/// * `Err(GitError)` - If the reset operation failed
|
|
||||||
pub fn reset(&self) -> Result<Self, GitError> {
|
|
||||||
// Check if repository exists and is a git repository
|
|
||||||
let git_dir = Path::new(&self.path).join(".git");
|
|
||||||
if !git_dir.exists() || !git_dir.is_dir() {
|
|
||||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset any local changes
|
|
||||||
let reset_output = Command::new("git")
|
|
||||||
.args(&["-C", &self.path, "reset", "--hard", "HEAD"])
|
|
||||||
.output()
|
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
|
||||||
|
|
||||||
if !reset_output.status.success() {
|
|
||||||
let error = String::from_utf8_lossy(&reset_output.stderr);
|
|
||||||
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean untracked files
|
|
||||||
let clean_output = Command::new("git")
|
|
||||||
.args(&["-C", &self.path, "clean", "-fd"])
|
|
||||||
.output()
|
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
|
||||||
|
|
||||||
if !clean_output.status.success() {
|
|
||||||
let error = String::from_utf8_lossy(&clean_output.stderr);
|
|
||||||
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commits changes in the repository.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `message` - The commit message
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
|
||||||
/// * `Err(GitError)` - If the commit operation failed
|
|
||||||
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
|
|
||||||
// Check if repository exists and is a git repository
|
|
||||||
let git_dir = Path::new(&self.path).join(".git");
|
|
||||||
if !git_dir.exists() || !git_dir.is_dir() {
|
|
||||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for local changes
|
|
||||||
if !self.has_changes()? {
|
|
||||||
return Ok(self.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add all changes
|
|
||||||
let add_output = Command::new("git")
|
|
||||||
.args(&["-C", &self.path, "add", "."])
|
|
||||||
.output()
|
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
|
||||||
|
|
||||||
if !add_output.status.success() {
|
|
||||||
let error = String::from_utf8_lossy(&add_output.stderr);
|
|
||||||
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit the changes
|
|
||||||
let commit_output = Command::new("git")
|
|
||||||
.args(&["-C", &self.path, "commit", "-m", message])
|
|
||||||
.output()
|
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
|
||||||
|
|
||||||
if !commit_output.status.success() {
|
|
||||||
let error = String::from_utf8_lossy(&commit_output.stderr);
|
|
||||||
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pushes changes to the remote repository.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
|
||||||
/// * `Err(GitError)` - If the push operation failed
|
|
||||||
pub fn push(&self) -> Result<Self, GitError> {
|
|
||||||
// Check if repository exists and is a git repository
|
|
||||||
let git_dir = Path::new(&self.path).join(".git");
|
|
||||||
if !git_dir.exists() || !git_dir.is_dir() {
|
|
||||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the changes
|
|
||||||
let push_output = Command::new("git")
|
|
||||||
.args(&["-C", &self.path, "push"])
|
|
||||||
.output()
|
|
||||||
.map_err(GitError::CommandExecutionError)?;
|
|
||||||
|
|
||||||
if push_output.status.success() {
|
|
||||||
Ok(self.clone())
|
|
||||||
} else {
|
|
||||||
let error = String::from_utf8_lossy(&push_output.stderr);
|
|
||||||
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement Clone for GitRepo to allow for method chaining
|
/**
|
||||||
impl Clone for GitRepo {
|
* Force updates a git repository by discarding local changes and pulling the latest changes.
|
||||||
fn clone(&self) -> Self {
|
*
|
||||||
GitRepo {
|
* This function will reset any uncommitted changes and clean untracked files before pulling.
|
||||||
path: self.path.clone(),
|
*
|
||||||
}
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating the repository was force-updated
|
||||||
|
* * `Err(GitError)` - An error if the update failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = git_update_force("my-project")?;
|
||||||
|
* println!("{}", result); // "Successfully force-updated repository at /home/user/code/github.com/user/my-project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_update_force(repo_path: &str) -> Result<String, GitError> {
|
||||||
|
// If repo_path may be a partial path, find the matching repository
|
||||||
|
let repos = find_matching_repos(repo_path)?;
|
||||||
|
|
||||||
|
// Should only be one repository at this point
|
||||||
|
let actual_path = &repos[0];
|
||||||
|
|
||||||
|
// Check if repository exists and is a git repository
|
||||||
|
let git_dir = Path::new(actual_path).join(".git");
|
||||||
|
if !git_dir.exists() || !git_dir.is_dir() {
|
||||||
|
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset any local changes
|
||||||
|
let reset_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "reset", "--hard", "HEAD"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !reset_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&reset_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean untracked files
|
||||||
|
let clean_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "clean", "-fd"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !clean_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&clean_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the latest changes
|
||||||
|
let pull_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "pull"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if pull_output.status.success() {
|
||||||
|
Ok(format!("Successfully force-updated repository at {}", actual_path))
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&pull_output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits changes in a git repository and then updates it by pulling the latest changes.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||||
|
* * `message` - The commit message
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating the repository was committed and updated
|
||||||
|
* * `Err(GitError)` - An error if the operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = git_update_commit("my-project", "Fix bug in login form")?;
|
||||||
|
* println!("{}", result); // "Successfully committed and updated repository at /home/user/code/github.com/user/my-project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_update_commit(repo_path: &str, message: &str) -> Result<String, GitError> {
|
||||||
|
// If repo_path may be a partial path, find the matching repository
|
||||||
|
let repos = find_matching_repos(repo_path)?;
|
||||||
|
|
||||||
|
// Should only be one repository at this point
|
||||||
|
let actual_path = &repos[0];
|
||||||
|
|
||||||
|
// Check if repository exists and is a git repository
|
||||||
|
let git_dir = Path::new(actual_path).join(".git");
|
||||||
|
if !git_dir.exists() || !git_dir.is_dir() {
|
||||||
|
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for local changes
|
||||||
|
if !has_git_changes(actual_path)? {
|
||||||
|
return Ok(format!("No changes to commit in repository at {}", actual_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all changes
|
||||||
|
let add_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "add", "."])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !add_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&add_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the changes
|
||||||
|
let commit_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "commit", "-m", message])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !commit_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&commit_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the latest changes
|
||||||
|
let pull_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "pull"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if pull_output.status.success() {
|
||||||
|
Ok(format!("Successfully committed and updated repository at {}", actual_path))
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&pull_output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits changes in a git repository and pushes them to the remote.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||||
|
* * `message` - The commit message
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating the repository was committed and pushed
|
||||||
|
* * `Err(GitError)` - An error if the operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = git_update_commit_push("my-project", "Add new feature")?;
|
||||||
|
* println!("{}", result); // "Successfully committed and pushed repository at /home/user/code/github.com/user/my-project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result<String, GitError> {
|
||||||
|
// If repo_path may be a partial path, find the matching repository
|
||||||
|
let repos = find_matching_repos(repo_path)?;
|
||||||
|
|
||||||
|
// Should only be one repository at this point
|
||||||
|
let actual_path = &repos[0];
|
||||||
|
|
||||||
|
// Check if repository exists and is a git repository
|
||||||
|
let git_dir = Path::new(actual_path).join(".git");
|
||||||
|
if !git_dir.exists() || !git_dir.is_dir() {
|
||||||
|
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for local changes
|
||||||
|
if !has_git_changes(actual_path)? {
|
||||||
|
return Ok(format!("No changes to commit in repository at {}", actual_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all changes
|
||||||
|
let add_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "add", "."])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !add_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&add_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the changes
|
||||||
|
let commit_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "commit", "-m", message])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !commit_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&commit_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the changes
|
||||||
|
let push_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "push"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if push_output.status.success() {
|
||||||
|
Ok(format!("Successfully committed and pushed repository at {}", actual_path))
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&push_output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ impl GitExecutor {
|
|||||||
// Get authentication configuration for a git URL
|
// Get authentication configuration for a git URL
|
||||||
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
|
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
|
||||||
if let Some(config) = &self.config {
|
if let Some(config) = &self.config {
|
||||||
let (server, _, _) = crate::git::git::parse_git_url(url);
|
let (server, _, _) = parse_git_url(url);
|
||||||
if !server.is_empty() {
|
if !server.is_empty() {
|
||||||
return config.auth.get(&server);
|
return config.auth.get(&server);
|
||||||
}
|
}
|
||||||
|
@ -1,212 +0,0 @@
|
|||||||
# Git Interface Redesign Plan
|
|
||||||
|
|
||||||
## Current Understanding
|
|
||||||
|
|
||||||
The current git interface consists of standalone functions like `git_clone`, `git_list`, `git_update`, etc. We want to replace this with an object-oriented interface using a builder pattern that allows for method chaining.
|
|
||||||
|
|
||||||
## New Interface Design
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
class GitTree {
|
|
||||||
+String base_path
|
|
||||||
+new(base_path: &str) Result<GitTree, GitError>
|
|
||||||
+list() Result<Vec<String>, GitError>
|
|
||||||
+find(pattern: &str) Result<Vec<String>, GitError>
|
|
||||||
+get(path_pattern: &str) Result<Vec<GitRepo>, GitError>
|
|
||||||
}
|
|
||||||
|
|
||||||
class GitRepo {
|
|
||||||
+String path
|
|
||||||
+pull() Result<GitRepo, GitError>
|
|
||||||
+reset() Result<GitRepo, GitError>
|
|
||||||
+push() Result<GitRepo, GitError>
|
|
||||||
+commit(message: &str) Result<GitRepo, GitError>
|
|
||||||
+has_changes() Result<bool, GitError>
|
|
||||||
}
|
|
||||||
|
|
||||||
GitTree --> GitRepo : creates
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementation Details
|
|
||||||
|
|
||||||
1. **GitTree Class**:
|
|
||||||
- Constructor takes a base path parameter that specifies where all git repositories will be located
|
|
||||||
- Methods for listing and finding repositories
|
|
||||||
- A `get()` method that returns one or more GitRepo objects based on a path pattern
|
|
||||||
- The `get()` method can also accept a URL (git or http format) and will clone the repository if it doesn't exist
|
|
||||||
|
|
||||||
2. **GitRepo Class**:
|
|
||||||
- Represents a single git repository
|
|
||||||
- Methods for common git operations: pull, reset, push, commit
|
|
||||||
- Each method returns a Result containing either the GitRepo object (for chaining) or an error
|
|
||||||
- If an operation fails, subsequent operations in the chain are skipped
|
|
||||||
|
|
||||||
3. **Error Handling**:
|
|
||||||
- Each method returns a Result type for immediate error handling
|
|
||||||
- Errors are propagated up the call chain
|
|
||||||
- The existing GitError enum will be reused
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### 1. Create the GitTree and GitRepo Structs in git.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct GitTree {
|
|
||||||
base_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GitRepo {
|
|
||||||
path: String,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Implement the GitTree Methods
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl GitTree {
|
|
||||||
pub fn new(base_path: &str) -> Result<Self, GitError> {
|
|
||||||
// Validate the base path
|
|
||||||
// Create the directory if it doesn't exist
|
|
||||||
Ok(GitTree {
|
|
||||||
base_path: base_path.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list(&self) -> Result<Vec<String>, GitError> {
|
|
||||||
// List all git repositories under the base path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> {
|
|
||||||
// Find repositories matching the pattern
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, path_pattern: &str) -> Result<Vec<GitRepo>, GitError> {
|
|
||||||
// Find repositories matching the pattern
|
|
||||||
// Return GitRepo objects for each match
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Implement the GitRepo Methods
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl GitRepo {
|
|
||||||
pub fn pull(&self) -> Result<Self, GitError> {
|
|
||||||
// Pull the latest changes
|
|
||||||
// Return self for chaining or an error
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(&self) -> Result<Self, GitError> {
|
|
||||||
// Reset any local changes
|
|
||||||
// Return self for chaining or an error
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(&self) -> Result<Self, GitError> {
|
|
||||||
// Push changes to the remote
|
|
||||||
// Return self for chaining or an error
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
|
|
||||||
// Commit changes with the given message
|
|
||||||
// Return self for chaining or an error
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_changes(&self) -> Result<bool, GitError> {
|
|
||||||
// Check if the repository has uncommitted changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Update the Rhai Wrappers in rhai/git.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Register the GitTree and GitRepo types with Rhai
|
|
||||||
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
|
||||||
// Register the GitTree type
|
|
||||||
engine.register_type::<GitTree>();
|
|
||||||
engine.register_fn("new", git_tree_new);
|
|
||||||
|
|
||||||
// Register GitTree methods
|
|
||||||
engine.register_fn("list", git_tree_list);
|
|
||||||
engine.register_fn("find", git_tree_find);
|
|
||||||
engine.register_fn("get", git_tree_get);
|
|
||||||
|
|
||||||
// Register GitRepo methods
|
|
||||||
engine.register_type::<GitRepo>();
|
|
||||||
engine.register_fn("pull", git_repo_pull);
|
|
||||||
engine.register_fn("reset", git_repo_reset);
|
|
||||||
engine.register_fn("push", git_repo_push);
|
|
||||||
engine.register_fn("commit", git_repo_commit);
|
|
||||||
engine.register_fn("has_changes", git_repo_has_changes);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Update Tests and Examples
|
|
||||||
|
|
||||||
- Update the test files to use the new interface
|
|
||||||
- Create new examples demonstrating the builder pattern and method chaining
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Example 1: Basic Repository Operations
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Create a new GitTree object
|
|
||||||
let git_tree = new("/home/user/code");
|
|
||||||
|
|
||||||
// List all repositories
|
|
||||||
let repos = git_tree.list();
|
|
||||||
print(`Found ${repos.len()} repositories`);
|
|
||||||
|
|
||||||
// Find repositories matching a pattern
|
|
||||||
let matching = git_tree.find("my-project*");
|
|
||||||
print(`Found ${matching.len()} matching repositories`);
|
|
||||||
|
|
||||||
// Get a repository and perform operations
|
|
||||||
let repo = git_tree.get("my-project")[0];
|
|
||||||
let result = repo.pull().reset().commit("Update files").push();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: Working with Multiple Repositories
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Create a new GitTree object
|
|
||||||
let git_tree = new("/home/user/code");
|
|
||||||
|
|
||||||
// Get all repositories matching a pattern
|
|
||||||
let repos = git_tree.get("project*");
|
|
||||||
print(`Found ${repos.len()} matching repositories`);
|
|
||||||
|
|
||||||
// Perform operations on all repositories
|
|
||||||
for repo in repos {
|
|
||||||
let result = repo.pull();
|
|
||||||
if result.is_ok() {
|
|
||||||
print(`Successfully pulled ${repo.path}`);
|
|
||||||
} else {
|
|
||||||
print(`Failed to pull ${repo.path}: ${result.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 3: Cloning a Repository
|
|
||||||
|
|
||||||
```rhai
|
|
||||||
// Create a new GitTree object
|
|
||||||
let git_tree = new("/home/user/code");
|
|
||||||
|
|
||||||
// Clone a repository by URL
|
|
||||||
let repos = git_tree.get("https://github.com/username/repo.git");
|
|
||||||
let repo = repos[0];
|
|
||||||
print(`Repository cloned to: ${repo.path}`);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
1. Implement the new interface in git.rs and rhai/git.rs
|
|
||||||
2. Update all tests and examples to use the new interface
|
|
||||||
3. Remove the old standalone functions
|
|
@ -98,35 +98,16 @@ fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfil
|
|||||||
let (ext, interpreter) = (".bat", "cmd.exe".to_string());
|
let (ext, interpreter) = (".bat", "cmd.exe".to_string());
|
||||||
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
let (ext, interpreter) = (".sh", "/bin/bash".to_string());
|
let (ext, interpreter) = (".sh", "/bin/sh".to_string());
|
||||||
|
|
||||||
// Create the script file
|
// Create the script file
|
||||||
let script_path = temp_dir.path().join(format!("script{}", ext));
|
let script_path = temp_dir.path().join(format!("script{}", ext));
|
||||||
let mut file = File::create(&script_path)
|
let mut file = File::create(&script_path)
|
||||||
.map_err(RunError::FileCreationFailed)?;
|
.map_err(RunError::FileCreationFailed)?;
|
||||||
|
|
||||||
// For Unix systems, ensure the script has a shebang line with -e flag
|
// Write the script content
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
file.write_all(dedented.as_bytes())
|
||||||
{
|
.map_err(RunError::FileWriteFailed)?;
|
||||||
let script_with_shebang = if dedented.trim_start().starts_with("#!") {
|
|
||||||
// Script already has a shebang, use it as is
|
|
||||||
dedented
|
|
||||||
} else {
|
|
||||||
// Add shebang with -e flag to ensure script fails on errors
|
|
||||||
format!("#!/bin/bash -e\n{}", dedented)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write the script content with shebang
|
|
||||||
file.write_all(script_with_shebang.as_bytes())
|
|
||||||
.map_err(RunError::FileWriteFailed)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For Windows, just write the script as is
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
file.write_all(dedented.as_bytes())
|
|
||||||
.map_err(RunError::FileWriteFailed)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the script executable (Unix only)
|
// Make the script executable (Unix only)
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
@ -185,8 +166,7 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
|
|||||||
if let Ok(l) = line {
|
if let Ok(l) = line {
|
||||||
// Print the line if not silent and flush immediately
|
// Print the line if not silent and flush immediately
|
||||||
if !silent_clone {
|
if !silent_clone {
|
||||||
// Always print stderr, even if silent is true, for error visibility
|
eprintln!("{}", l);
|
||||||
eprintln!("\x1b[31mERROR: {}\x1b[0m", l); // Red color for errors
|
|
||||||
std::io::stderr().flush().unwrap_or(());
|
std::io::stderr().flush().unwrap_or(());
|
||||||
}
|
}
|
||||||
// Store it in our captured buffer
|
// Store it in our captured buffer
|
||||||
@ -218,14 +198,6 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
|
|||||||
"Failed to capture stderr".to_string()
|
"Failed to capture stderr".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the command failed, print the stderr if it wasn't already printed
|
|
||||||
if !status.success() && silent && !captured_stderr.is_empty() {
|
|
||||||
eprintln!("\x1b[31mCommand failed with error:\x1b[0m");
|
|
||||||
for line in captured_stderr.lines() {
|
|
||||||
eprintln!("\x1b[31m{}\x1b[0m", line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the command result
|
// Return the command result
|
||||||
Ok(CommandResult {
|
Ok(CommandResult {
|
||||||
stdout: captured_stdout,
|
stdout: captured_stdout,
|
||||||
@ -242,20 +214,6 @@ fn process_command_output(output: Result<Output, std::io::Error>) -> Result<Comm
|
|||||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||||
|
|
||||||
// Print stderr if there's any, even for silent execution
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
eprintln!("\x1b[31mCommand stderr output:\x1b[0m");
|
|
||||||
for line in stderr.lines() {
|
|
||||||
eprintln!("\x1b[31m{}\x1b[0m", line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the command failed, print a clear error message
|
|
||||||
if !out.status.success() {
|
|
||||||
eprintln!("\x1b[31mCommand failed with exit code: {}\x1b[0m",
|
|
||||||
out.status.code().unwrap_or(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(CommandResult {
|
Ok(CommandResult {
|
||||||
stdout,
|
stdout,
|
||||||
stderr,
|
stderr,
|
||||||
@ -302,18 +260,7 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
|
|||||||
.args(&command_args)
|
.args(&command_args)
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let result = process_command_output(output)?;
|
process_command_output(output)
|
||||||
|
|
||||||
// If the script failed, return an error
|
|
||||||
if !result.success {
|
|
||||||
return Err(RunError::CommandFailed(format!(
|
|
||||||
"Script execution failed with exit code {}: {}",
|
|
||||||
result.code,
|
|
||||||
result.stderr.trim()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
} else {
|
} else {
|
||||||
// For normal execution, spawn and handle the output streams
|
// For normal execution, spawn and handle the output streams
|
||||||
let child = Command::new(interpreter)
|
let child = Command::new(interpreter)
|
||||||
@ -323,45 +270,16 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
|
|||||||
.spawn()
|
.spawn()
|
||||||
.map_err(RunError::CommandExecutionFailed)?;
|
.map_err(RunError::CommandExecutionFailed)?;
|
||||||
|
|
||||||
let result = handle_child_output(child, false)?;
|
handle_child_output(child, false)
|
||||||
|
|
||||||
// If the script failed, return an error
|
|
||||||
if !result.success {
|
|
||||||
return Err(RunError::CommandFailed(format!(
|
|
||||||
"Script execution failed with exit code {}: {}",
|
|
||||||
result.code,
|
|
||||||
result.stderr.trim()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a multiline script with optional silent mode
|
/// Run a multiline script with optional silent mode
|
||||||
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
|
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
|
||||||
// Print the script being executed if not silent
|
|
||||||
if !silent {
|
|
||||||
println!("\x1b[36mExecuting script:\x1b[0m");
|
|
||||||
for (i, line) in script.lines().enumerate() {
|
|
||||||
println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
|
|
||||||
}
|
|
||||||
println!("\x1b[36m---\x1b[0m");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
|
let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
|
||||||
// _temp_dir is kept in scope until the end of this function to ensure
|
// _temp_dir is kept in scope until the end of this function to ensure
|
||||||
// it's not dropped prematurely, which would clean up the directory
|
// it's not dropped prematurely, which would clean up the directory
|
||||||
|
execute_script_internal(&interpreter, &script_path, silent)
|
||||||
// Execute the script and handle the result
|
|
||||||
let result = execute_script_internal(&interpreter, &script_path, silent);
|
|
||||||
|
|
||||||
// If there was an error, print a clear error message
|
|
||||||
if let Err(ref e) = result {
|
|
||||||
eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A builder for configuring and executing commands or scripts
|
/// A builder for configuring and executing commands or scripts
|
||||||
@ -420,41 +338,21 @@ impl<'a> RunBuilder<'a> {
|
|||||||
|
|
||||||
// Log command execution if enabled
|
// Log command execution if enabled
|
||||||
if self.log {
|
if self.log {
|
||||||
println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed);
|
println!("[LOG] Executing command: {}", trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle async execution
|
// Handle async execution
|
||||||
if self.async_exec {
|
if self.async_exec {
|
||||||
let cmd_copy = trimmed.to_string();
|
let cmd_copy = trimmed.to_string();
|
||||||
let silent = self.silent;
|
let silent = self.silent;
|
||||||
let log = self.log;
|
|
||||||
|
|
||||||
// Spawn a thread to run the command asynchronously
|
// Spawn a thread to run the command asynchronously
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
if log {
|
let _ = if cmd_copy.contains('\n') {
|
||||||
println!("\x1b[36m[ASYNC] Starting execution\x1b[0m");
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = if cmd_copy.contains('\n') {
|
|
||||||
run_script_internal(&cmd_copy, silent)
|
run_script_internal(&cmd_copy, silent)
|
||||||
} else {
|
} else {
|
||||||
run_command_internal(&cmd_copy, silent)
|
run_command_internal(&cmd_copy, silent)
|
||||||
};
|
};
|
||||||
|
|
||||||
if log {
|
|
||||||
match &result {
|
|
||||||
Ok(res) => {
|
|
||||||
if res.success {
|
|
||||||
println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
|
|
||||||
} else {
|
|
||||||
eprintln!("\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m", res.code);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return a placeholder result for async execution
|
// Return a placeholder result for async execution
|
||||||
@ -477,17 +375,8 @@ impl<'a> RunBuilder<'a> {
|
|||||||
|
|
||||||
// Handle die=false: convert errors to CommandResult with success=false
|
// Handle die=false: convert errors to CommandResult with success=false
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => {
|
Ok(res) => Ok(res),
|
||||||
// If the command failed but die is false, print a warning
|
|
||||||
if !res.success && !self.die && !self.silent {
|
|
||||||
eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Always print the error, even if die is false
|
|
||||||
eprintln!("\x1b[31mCommand error: {}\x1b[0m", e);
|
|
||||||
|
|
||||||
if self.die {
|
if self.die {
|
||||||
Err(e)
|
Err(e)
|
||||||
} else {
|
} else {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
|
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::virt::buildah::{self, BuildahError, Image, Builder};
|
use crate::virt::buildah::{self, BuildahError, Image};
|
||||||
use crate::process::CommandResult;
|
use crate::process::CommandResult;
|
||||||
|
|
||||||
/// Register Buildah module functions with the Rhai engine
|
/// Register Buildah module functions with the Rhai engine
|
||||||
@ -20,42 +20,35 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
|
|||||||
// Register types
|
// Register types
|
||||||
register_bah_types(engine)?;
|
register_bah_types(engine)?;
|
||||||
|
|
||||||
// Register Builder constructor
|
// Register container functions
|
||||||
engine.register_fn("bah_new", bah_new);
|
engine.register_fn("bah_from", bah_from);
|
||||||
|
engine.register_fn("bah_run", bah_run);
|
||||||
|
engine.register_fn("bah_run_with_isolation", bah_run_with_isolation);
|
||||||
|
engine.register_fn("bah_copy", bah_copy);
|
||||||
|
engine.register_fn("bah_add", bah_add);
|
||||||
|
engine.register_fn("bah_commit", bah_commit);
|
||||||
|
engine.register_fn("bah_remove", bah_remove);
|
||||||
|
engine.register_fn("bah_list", bah_list);
|
||||||
|
engine.register_fn("bah_build", bah_build_with_options);
|
||||||
|
engine.register_fn("bah_new_build_options", new_build_options);
|
||||||
|
|
||||||
// Register Builder instance methods
|
// Register image functions
|
||||||
engine.register_fn("run", builder_run);
|
engine.register_fn("bah_images", images);
|
||||||
engine.register_fn("run_with_isolation", builder_run_with_isolation);
|
engine.register_fn("bah_image_remove", image_remove);
|
||||||
engine.register_fn("copy", builder_copy);
|
engine.register_fn("bah_image_push", image_push);
|
||||||
engine.register_fn("add", builder_add);
|
engine.register_fn("bah_image_tag", image_tag);
|
||||||
engine.register_fn("commit", builder_commit);
|
engine.register_fn("bah_image_pull", image_pull);
|
||||||
// Remove the line that's causing the error
|
engine.register_fn("bah_image_commit", image_commit_with_options);
|
||||||
engine.register_fn("remove", builder_remove);
|
engine.register_fn("bah_new_commit_options", new_commit_options);
|
||||||
engine.register_fn("reset", builder_reset);
|
engine.register_fn("bah_config", config_with_options);
|
||||||
engine.register_fn("config", builder_config);
|
engine.register_fn("bah_new_config_options", new_config_options);
|
||||||
|
|
||||||
// Register Builder static methods
|
|
||||||
engine.register_fn("images", builder_images);
|
|
||||||
engine.register_fn("image_remove", builder_image_remove);
|
|
||||||
engine.register_fn("image_pull", builder_image_pull);
|
|
||||||
engine.register_fn("image_push", builder_image_push);
|
|
||||||
engine.register_fn("image_tag", builder_image_tag);
|
|
||||||
engine.register_fn("build", builder_build);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register Buildah module types with the Rhai engine
|
/// Register Buildah module types with the Rhai engine
|
||||||
fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||||
// Register Builder type
|
// Register Image type and methods
|
||||||
engine.register_type_with_name::<Builder>("BuildahBuilder");
|
|
||||||
|
|
||||||
// Register getters for Builder properties
|
|
||||||
engine.register_get("container_id", get_builder_container_id);
|
|
||||||
engine.register_get("name", get_builder_name);
|
|
||||||
engine.register_get("image", get_builder_image);
|
|
||||||
|
|
||||||
// Register Image type and methods (same as before)
|
|
||||||
engine.register_type_with_name::<Image>("BuildahImage");
|
engine.register_type_with_name::<Image>("BuildahImage");
|
||||||
|
|
||||||
// Register getters for Image properties
|
// Register getters for Image properties
|
||||||
@ -91,8 +84,312 @@ fn bah_error_to_rhai_error<T>(result: Result<T, BuildahError>) -> Result<T, Box<
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to convert Rhai Map to Rust HashMap
|
/// Create a new Map with default build options
|
||||||
fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<EvalAltResult>> {
|
pub fn new_build_options() -> Map {
|
||||||
|
let mut map = Map::new();
|
||||||
|
map.insert("tag".into(), Dynamic::UNIT);
|
||||||
|
map.insert("context_dir".into(), Dynamic::from("."));
|
||||||
|
map.insert("file".into(), Dynamic::from("Dockerfile"));
|
||||||
|
map.insert("isolation".into(), Dynamic::UNIT);
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Map with default commit options
|
||||||
|
pub fn new_commit_options() -> Map {
|
||||||
|
let mut map = Map::new();
|
||||||
|
map.insert("format".into(), Dynamic::UNIT);
|
||||||
|
map.insert("squash".into(), Dynamic::from(false));
|
||||||
|
map.insert("rm".into(), Dynamic::from(false));
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Map for config options
|
||||||
|
pub fn new_config_options() -> Map {
|
||||||
|
Map::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Container Function Wrappers
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Wrapper for buildah::from
|
||||||
|
///
|
||||||
|
/// Create a container from an image.
|
||||||
|
pub fn bah_from(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
let result = bah_error_to_rhai_error(buildah::from(image))?;
|
||||||
|
|
||||||
|
// Create a new CommandResult with trimmed stdout
|
||||||
|
let trimmed_result = CommandResult {
|
||||||
|
stdout: result.stdout.trim().to_string(),
|
||||||
|
stderr: result.stderr.trim().to_string(),
|
||||||
|
success: result.success,
|
||||||
|
code: result.code,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(trimmed_result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::run
|
||||||
|
///
|
||||||
|
/// Run a command in a container.
|
||||||
|
pub fn bah_run(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::run(container, command))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::run_with_isolation
|
||||||
|
///
|
||||||
|
/// Run a command in a container with specified isolation.
|
||||||
|
pub fn bah_run_with_isolation(container: &str, command: &str, isolation: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::bah_run_with_isolation(container, command, isolation))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::copy
|
||||||
|
///
|
||||||
|
/// Copy files into a container.
|
||||||
|
pub fn bah_copy(container: &str, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::bah_copy(container, source, dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::add
|
||||||
|
///
|
||||||
|
/// Add files into a container.
|
||||||
|
pub fn bah_add(container: &str, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::bah_add(container, source, dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::commit
|
||||||
|
///
|
||||||
|
/// Commit a container to an image.
|
||||||
|
pub fn bah_commit(container: &str, image_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::bah_commit(container, image_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::remove
|
||||||
|
///
|
||||||
|
/// Remove a container.
|
||||||
|
pub fn bah_remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::bah_remove(container))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::list
|
||||||
|
///
|
||||||
|
/// List containers.
|
||||||
|
pub fn bah_list() -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::bah_list())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build an image with options specified in a Map
|
||||||
|
///
|
||||||
|
/// This provides a builder-style interface for Rhai scripts.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rhai
|
||||||
|
/// let options = bah_new_build_options();
|
||||||
|
/// options.tag = "my-image:latest";
|
||||||
|
/// options.context_dir = ".";
|
||||||
|
/// options.file = "Dockerfile";
|
||||||
|
/// options.isolation = "chroot";
|
||||||
|
/// let result = bah_build(options);
|
||||||
|
/// ```
|
||||||
|
pub fn bah_build_with_options(options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
// Extract options from the map
|
||||||
|
let tag_option = match options.get("tag") {
|
||||||
|
Some(tag) => {
|
||||||
|
if tag.is_unit() {
|
||||||
|
None
|
||||||
|
} else if let Ok(tag_str) = tag.clone().into_string() {
|
||||||
|
Some(tag_str)
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
"tag must be a string".into(),
|
||||||
|
rhai::Position::NONE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
let context_dir = match options.get("context_dir") {
|
||||||
|
Some(dir) => {
|
||||||
|
if let Ok(dir_str) = dir.clone().into_string() {
|
||||||
|
dir_str
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
"context_dir must be a string".into(),
|
||||||
|
rhai::Position::NONE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => String::from(".")
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = match options.get("file") {
|
||||||
|
Some(file) => {
|
||||||
|
if let Ok(file_str) = file.clone().into_string() {
|
||||||
|
file_str
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
"file must be a string".into(),
|
||||||
|
rhai::Position::NONE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => String::from("Dockerfile")
|
||||||
|
};
|
||||||
|
|
||||||
|
let isolation_option = match options.get("isolation") {
|
||||||
|
Some(isolation) => {
|
||||||
|
if isolation.is_unit() {
|
||||||
|
None
|
||||||
|
} else if let Ok(isolation_str) = isolation.clone().into_string() {
|
||||||
|
Some(isolation_str)
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
"isolation must be a string".into(),
|
||||||
|
rhai::Position::NONE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert String to &str for the function call
|
||||||
|
let tag_ref = tag_option.as_deref();
|
||||||
|
let isolation_ref = isolation_option.as_deref();
|
||||||
|
|
||||||
|
// Call the buildah build function
|
||||||
|
bah_error_to_rhai_error(buildah::bah_build(tag_ref, &context_dir, &file, isolation_ref))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Image Function Wrappers
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Wrapper for buildah::images
|
||||||
|
///
|
||||||
|
/// List images in local storage.
|
||||||
|
pub fn images() -> Result<Array, Box<EvalAltResult>> {
|
||||||
|
let images = bah_error_to_rhai_error(buildah::images())?;
|
||||||
|
|
||||||
|
// Convert Vec<Image> to Rhai Array
|
||||||
|
let mut array = Array::new();
|
||||||
|
for image in images {
|
||||||
|
array.push(Dynamic::from(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(array)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::image_remove
|
||||||
|
///
|
||||||
|
/// Remove one or more images.
|
||||||
|
pub fn image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::image_remove(image))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::image_push
|
||||||
|
///
|
||||||
|
/// Push an image to a registry.
|
||||||
|
pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::image_push(image, destination, tls_verify))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::image_tag
|
||||||
|
///
|
||||||
|
/// Add an additional name to a local image.
|
||||||
|
pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::image_tag(image, new_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for buildah::image_pull
|
||||||
|
///
|
||||||
|
/// Pull an image from a registry.
|
||||||
|
pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
bah_error_to_rhai_error(buildah::image_pull(image, tls_verify))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit a container to an image with options specified in a Map
|
||||||
|
///
|
||||||
|
/// This provides a builder-style interface for Rhai scripts.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rhai
|
||||||
|
/// let options = bah_new_commit_options();
|
||||||
|
/// options.format = "docker";
|
||||||
|
/// options.squash = true;
|
||||||
|
/// options.rm = true;
|
||||||
|
/// let result = bah_image_commit("my-container", "my-image:latest", options);
|
||||||
|
/// ```
|
||||||
|
pub fn image_commit_with_options(container: &str, image_name: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
// Extract options from the map
|
||||||
|
let format_option = match options.get("format") {
|
||||||
|
Some(format) => {
|
||||||
|
if format.is_unit() {
|
||||||
|
None
|
||||||
|
} else if let Ok(format_str) = format.clone().into_string() {
|
||||||
|
Some(format_str)
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
"format must be a string".into(),
|
||||||
|
rhai::Position::NONE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
let squash = match options.get("squash") {
|
||||||
|
Some(squash) => {
|
||||||
|
if let Ok(squash_val) = squash.clone().as_bool() {
|
||||||
|
squash_val
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
"squash must be a boolean".into(),
|
||||||
|
rhai::Position::NONE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => false
|
||||||
|
};
|
||||||
|
|
||||||
|
let rm = match options.get("rm") {
|
||||||
|
Some(rm) => {
|
||||||
|
if let Ok(rm_val) = rm.clone().as_bool() {
|
||||||
|
rm_val
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
"rm must be a boolean".into(),
|
||||||
|
rhai::Position::NONE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert String to &str for the function call
|
||||||
|
let format_ref = format_option.as_deref();
|
||||||
|
|
||||||
|
// Call the buildah image_commit function
|
||||||
|
bah_error_to_rhai_error(buildah::image_commit(container, image_name, format_ref, squash, rm))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure a container with options specified in a Map
|
||||||
|
///
|
||||||
|
/// This provides a builder-style interface for Rhai scripts.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rhai
|
||||||
|
/// let options = bah_new_config_options();
|
||||||
|
/// options.author = "John Doe";
|
||||||
|
/// options.cmd = "echo Hello";
|
||||||
|
/// options.entrypoint = "/bin/sh -c";
|
||||||
|
/// let result = bah_config("my-container", options);
|
||||||
|
/// ```
|
||||||
|
pub fn config_with_options(container: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||||
|
// Convert Rhai Map to Rust HashMap
|
||||||
let mut config_options = HashMap::<String, String>::new();
|
let mut config_options = HashMap::<String, String>::new();
|
||||||
|
|
||||||
for (key, value) in options.iter() {
|
for (key, value) in options.iter() {
|
||||||
@ -107,96 +404,6 @@ fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config_options)
|
// Call the buildah config function
|
||||||
}
|
bah_error_to_rhai_error(buildah::bah_config(container, config_options))
|
||||||
|
|
||||||
/// Create a new Builder
|
|
||||||
pub fn bah_new(name: &str, image: &str) -> Result<Builder, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::new(name, image))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builder instance methods
|
|
||||||
pub fn builder_run(builder: &mut Builder, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.run(command))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_run_with_isolation(builder: &mut Builder, command: &str, isolation: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.run_with_isolation(command, isolation))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_copy(builder: &mut Builder, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.copy(source, dest))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_add(builder: &mut Builder, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.add(source, dest))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_commit(builder: &mut Builder, image_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.commit(image_name))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_remove(builder: &mut Builder) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.remove())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_config(builder: &mut Builder, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
// Convert Rhai Map to Rust HashMap
|
|
||||||
let config_options = convert_map_to_hashmap(options)?;
|
|
||||||
bah_error_to_rhai_error(builder.config(config_options))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builder static methods
|
|
||||||
pub fn builder_images(_builder: &mut Builder) -> Result<Array, Box<EvalAltResult>> {
|
|
||||||
let images = bah_error_to_rhai_error(Builder::images())?;
|
|
||||||
|
|
||||||
// Convert Vec<Image> to Rhai Array
|
|
||||||
let mut array = Array::new();
|
|
||||||
for image in images {
|
|
||||||
array.push(Dynamic::from(image));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(array)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_image_remove(_builder: &mut Builder, image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::image_remove(image))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_image_pull(_builder: &mut Builder, image: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::image_pull(image, tls_verify))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_image_push(_builder: &mut Builder, image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder_image_tag(_builder: &mut Builder, image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::image_tag(image, new_name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getter functions for Builder properties
|
|
||||||
pub fn get_builder_container_id(builder: &mut Builder) -> String {
|
|
||||||
match builder.container_id() {
|
|
||||||
Some(id) => id.clone(),
|
|
||||||
None => "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_builder_name(builder: &mut Builder) -> String {
|
|
||||||
builder.name().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_builder_image(builder: &mut Builder) -> String {
|
|
||||||
builder.image().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset function for Builder
|
|
||||||
pub fn builder_reset(builder: &mut Builder) -> Result<(), Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(builder.reset())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build function for Builder
|
|
||||||
pub fn builder_build(_builder: &mut Builder, tag: &str, context_dir: &str, file: &str, isolation: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
|
||||||
bah_error_to_rhai_error(Builder::build(Some(tag), context_dir, file, Some(isolation)))
|
|
||||||
}
|
}
|
122
src/rhai/git.rs
122
src/rhai/git.rs
@ -3,7 +3,7 @@
|
|||||||
//! This module provides Rhai wrappers for the functions in the Git module.
|
//! This module provides Rhai wrappers for the functions in the Git module.
|
||||||
|
|
||||||
use rhai::{Engine, EvalAltResult, Array, Dynamic};
|
use rhai::{Engine, EvalAltResult, Array, Dynamic};
|
||||||
use crate::git::{GitTree, GitRepo, GitError};
|
use crate::git::{self, GitError};
|
||||||
|
|
||||||
/// Register Git module functions with the Rhai engine
|
/// Register Git module functions with the Rhai engine
|
||||||
///
|
///
|
||||||
@ -15,21 +15,15 @@ use crate::git::{GitTree, GitRepo, GitError};
|
|||||||
///
|
///
|
||||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
||||||
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||||
// Register GitTree constructor
|
// Register basic git functions
|
||||||
engine.register_fn("gittree_new", git_tree_new);
|
engine.register_fn("git_clone", git_clone);
|
||||||
|
engine.register_fn("git_list", git_list);
|
||||||
// Register GitTree methods
|
engine.register_fn("git_update", git_update);
|
||||||
engine.register_fn("list", git_tree_list);
|
engine.register_fn("git_update_force", git_update_force);
|
||||||
engine.register_fn("find", git_tree_find);
|
engine.register_fn("git_update_commit", git_update_commit);
|
||||||
engine.register_fn("get", git_tree_get);
|
engine.register_fn("git_update_commit_push", git_update_commit_push);
|
||||||
|
engine.register_fn("git_has_changes", has_git_changes);
|
||||||
// Register GitRepo methods
|
engine.register_fn("git_find_repos", find_matching_repos);
|
||||||
engine.register_fn("path", git_repo_path);
|
|
||||||
engine.register_fn("has_changes", git_repo_has_changes);
|
|
||||||
engine.register_fn("pull", git_repo_pull);
|
|
||||||
engine.register_fn("reset", git_repo_reset);
|
|
||||||
engine.register_fn("commit", git_repo_commit);
|
|
||||||
engine.register_fn("push", git_repo_push);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -45,21 +39,21 @@ fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<Eval
|
|||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// GitTree Function Wrappers
|
// Git Function Wrappers
|
||||||
//
|
//
|
||||||
|
|
||||||
/// Wrapper for GitTree::new
|
/// Wrapper for git::git_clone
|
||||||
///
|
///
|
||||||
/// Creates a new GitTree with the specified base path.
|
/// Clones a git repository to a standardized location in the user's home directory.
|
||||||
pub fn git_tree_new(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> {
|
pub fn git_clone(url: &str) -> Result<String, Box<EvalAltResult>> {
|
||||||
git_error_to_rhai_error(GitTree::new(base_path))
|
git_error_to_rhai_error(git::git_clone(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for GitTree::list
|
/// Wrapper for git::git_list
|
||||||
///
|
///
|
||||||
/// Lists all git repositories under the base path.
|
/// Lists all git repositories found in the user's ~/code directory.
|
||||||
pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> {
|
pub fn git_list() -> Result<Array, Box<EvalAltResult>> {
|
||||||
let repos = git_error_to_rhai_error(git_tree.list())?;
|
let repos = git_error_to_rhai_error(git::git_list())?;
|
||||||
|
|
||||||
// Convert Vec<String> to Rhai Array
|
// Convert Vec<String> to Rhai Array
|
||||||
let mut array = Array::new();
|
let mut array = Array::new();
|
||||||
@ -70,11 +64,18 @@ pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>
|
|||||||
Ok(array)
|
Ok(array)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for GitTree::find
|
/// Wrapper for git::has_git_changes
|
||||||
|
///
|
||||||
|
/// Checks if a git repository has uncommitted changes.
|
||||||
|
pub fn has_git_changes(repo_path: &str) -> Result<bool, Box<EvalAltResult>> {
|
||||||
|
git_error_to_rhai_error(git::has_git_changes(repo_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for git::find_matching_repos
|
||||||
///
|
///
|
||||||
/// Finds repositories matching a pattern or partial path.
|
/// Finds repositories matching a pattern or partial path.
|
||||||
pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> {
|
pub fn find_matching_repos(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||||
let repos = git_error_to_rhai_error(git_tree.find(pattern))?;
|
let repos = git_error_to_rhai_error(git::find_matching_repos(pattern))?;
|
||||||
|
|
||||||
// Convert Vec<String> to Rhai Array
|
// Convert Vec<String> to Rhai Array
|
||||||
let mut array = Array::new();
|
let mut array = Array::new();
|
||||||
@ -85,63 +86,30 @@ pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box
|
|||||||
Ok(array)
|
Ok(array)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for GitTree::get
|
/// Wrapper for git::git_update
|
||||||
///
|
///
|
||||||
/// Gets one or more GitRepo objects based on a path pattern or URL.
|
/// Updates a git repository by pulling the latest changes.
|
||||||
pub fn git_tree_get(git_tree: &mut GitTree, path_or_url: &str) -> Result<Array, Box<EvalAltResult>> {
|
pub fn git_update(repo_path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||||
let repos = git_error_to_rhai_error(git_tree.get(path_or_url))?;
|
git_error_to_rhai_error(git::git_update(repo_path))
|
||||||
|
|
||||||
// Convert Vec<GitRepo> to Rhai Array
|
|
||||||
let mut array = Array::new();
|
|
||||||
for repo in repos {
|
|
||||||
array.push(Dynamic::from(repo));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(array)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
/// Wrapper for git::git_update_force
|
||||||
// GitRepo Function Wrappers
|
|
||||||
//
|
|
||||||
|
|
||||||
/// Wrapper for GitRepo::path
|
|
||||||
///
|
///
|
||||||
/// Gets the path of the repository.
|
/// Force updates a git repository by discarding local changes and pulling the latest changes.
|
||||||
pub fn git_repo_path(git_repo: &mut GitRepo) -> String {
|
pub fn git_update_force(repo_path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||||
git_repo.path().to_string()
|
git_error_to_rhai_error(git::git_update_force(repo_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for GitRepo::has_changes
|
/// Wrapper for git::git_update_commit
|
||||||
///
|
///
|
||||||
/// Checks if the repository has uncommitted changes.
|
/// Commits changes in a git repository and then updates it by pulling the latest changes.
|
||||||
pub fn git_repo_has_changes(git_repo: &mut GitRepo) -> Result<bool, Box<EvalAltResult>> {
|
pub fn git_update_commit(repo_path: &str, message: &str) -> Result<String, Box<EvalAltResult>> {
|
||||||
git_error_to_rhai_error(git_repo.has_changes())
|
git_error_to_rhai_error(git::git_update_commit(repo_path, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for GitRepo::pull
|
/// Wrapper for git::git_update_commit_push
|
||||||
///
|
///
|
||||||
/// Pulls the latest changes from the remote repository.
|
/// Commits changes in a git repository and pushes them to the remote.
|
||||||
pub fn git_repo_pull(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
|
pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result<String, Box<EvalAltResult>> {
|
||||||
git_error_to_rhai_error(git_repo.pull())
|
git_error_to_rhai_error(git::git_update_commit_push(repo_path, message))
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper for GitRepo::reset
|
|
||||||
///
|
|
||||||
/// Resets any local changes in the repository.
|
|
||||||
pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
|
|
||||||
git_error_to_rhai_error(git_repo.reset())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper for GitRepo::commit
|
|
||||||
///
|
|
||||||
/// Commits changes in the repository.
|
|
||||||
pub fn git_repo_commit(git_repo: &mut GitRepo, message: &str) -> Result<GitRepo, Box<EvalAltResult>> {
|
|
||||||
git_error_to_rhai_error(git_repo.commit(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper for GitRepo::push
|
|
||||||
///
|
|
||||||
/// Pushes changes to the remote repository.
|
|
||||||
pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
|
|
||||||
git_error_to_rhai_error(git_repo.push())
|
|
||||||
}
|
}
|
@ -39,22 +39,25 @@ pub use process::{
|
|||||||
|
|
||||||
// Re-export buildah functions
|
// Re-export buildah functions
|
||||||
pub use buildah::register_bah_module;
|
pub use buildah::register_bah_module;
|
||||||
pub use buildah::bah_new;
|
pub use buildah::{
|
||||||
|
bah_from, bah_run, bah_run_with_isolation, bah_copy, bah_add, bah_commit,
|
||||||
|
bah_remove, bah_list, bah_build_with_options,
|
||||||
|
new_commit_options, new_config_options, image_commit_with_options, config_with_options
|
||||||
|
};
|
||||||
|
|
||||||
// Re-export nerdctl functions
|
// Re-export nerdctl functions
|
||||||
pub use nerdctl::register_nerdctl_module;
|
pub use nerdctl::register_nerdctl_module;
|
||||||
pub use nerdctl::{
|
pub use nerdctl::{
|
||||||
// Container functions
|
nerdctl_run, nerdctl_exec,
|
||||||
nerdctl_run, nerdctl_run_with_name, nerdctl_run_with_port,
|
nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list
|
||||||
nerdctl_exec, nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list,
|
|
||||||
// Image functions
|
|
||||||
nerdctl_images, nerdctl_image_remove, nerdctl_image_push, nerdctl_image_tag,
|
|
||||||
nerdctl_image_pull, nerdctl_image_commit, nerdctl_image_build
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export git module
|
// Re-export git functions
|
||||||
pub use git::register_git_module;
|
pub use git::register_git_module;
|
||||||
pub use crate::git::{GitTree, GitRepo};
|
pub use git::{
|
||||||
|
git_clone, git_list, git_update, git_update_force, git_update_commit,
|
||||||
|
git_update_commit_push, has_git_changes, find_matching_repos
|
||||||
|
};
|
||||||
|
|
||||||
// Rename copy functions to avoid conflicts
|
// Rename copy functions to avoid conflicts
|
||||||
pub use os::copy as os_copy;
|
pub use os::copy as os_copy;
|
||||||
|
131
src/rhai/tests/git_test.rhai
Normal file
131
src/rhai/tests/git_test.rhai
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Test script for Git module functions
|
||||||
|
|
||||||
|
// Import required modules
|
||||||
|
import "os" as os;
|
||||||
|
import "process" as process;
|
||||||
|
|
||||||
|
// Test git_clone function
|
||||||
|
fn test_git_clone() {
|
||||||
|
// Use a public repository for testing
|
||||||
|
let repo_url = "https://github.com/rhaiscript/rhai.git";
|
||||||
|
|
||||||
|
// Clone the repository
|
||||||
|
print("Testing git_clone...");
|
||||||
|
let result = git_clone(repo_url);
|
||||||
|
|
||||||
|
// Print the result
|
||||||
|
print(`Clone result: ${result}`);
|
||||||
|
|
||||||
|
// Verify the repository exists
|
||||||
|
if result.contains("already exists") {
|
||||||
|
print("Repository already exists, test passed");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path exists
|
||||||
|
if exist(result) {
|
||||||
|
print("Repository cloned successfully, test passed");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Repository clone failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test git_list function
|
||||||
|
fn test_git_list() {
|
||||||
|
print("Testing git_list...");
|
||||||
|
let repos = git_list();
|
||||||
|
|
||||||
|
print(`Found ${repos.len()} repositories`);
|
||||||
|
|
||||||
|
// Print the first few repositories
|
||||||
|
let count = if repos.len() > 3 { 3 } else { repos.len() };
|
||||||
|
for i in range(0, count) {
|
||||||
|
print(` - ${repos[i]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return repos.len() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test git_has_changes function
|
||||||
|
fn test_git_has_changes() {
|
||||||
|
print("Testing git_has_changes...");
|
||||||
|
|
||||||
|
// Get a repository from the list
|
||||||
|
let repos = git_list();
|
||||||
|
if repos.len() == 0 {
|
||||||
|
print("No repositories found, skipping test");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let repo = repos[0];
|
||||||
|
let has_changes = git_has_changes(repo);
|
||||||
|
|
||||||
|
print(`Repository ${repo} has changes: ${has_changes}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test find_matching_repos function
|
||||||
|
fn test_find_matching_repos() {
|
||||||
|
print("Testing find_matching_repos...");
|
||||||
|
|
||||||
|
// Get all repositories with wildcard
|
||||||
|
let all_repos = git_list();
|
||||||
|
if all_repos.len() == 0 {
|
||||||
|
print("No repositories found, skipping test");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract a part of the first repo name to search for
|
||||||
|
let repo_name = all_repos[0].split("/").last();
|
||||||
|
let search_pattern = repo_name.substring(0, 3) + "*";
|
||||||
|
|
||||||
|
print(`Searching for repositories matching pattern: ${search_pattern}`);
|
||||||
|
let matching = find_matching_repos(search_pattern);
|
||||||
|
|
||||||
|
print(`Found ${matching.len()} matching repositories`);
|
||||||
|
for repo in matching {
|
||||||
|
print(` - ${repo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matching.len() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the tests
|
||||||
|
fn run_tests() {
|
||||||
|
let tests = [
|
||||||
|
#{ name: "git_clone", fn: test_git_clone },
|
||||||
|
#{ name: "git_list", fn: test_git_list },
|
||||||
|
#{ name: "git_has_changes", fn: test_git_has_changes },
|
||||||
|
#{ name: "find_matching_repos", fn: test_find_matching_repos }
|
||||||
|
];
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
print(`\nRunning test: ${test.name}`);
|
||||||
|
|
||||||
|
let result = false;
|
||||||
|
try {
|
||||||
|
result = test.fn();
|
||||||
|
} catch(err) {
|
||||||
|
print(`Test ${test.name} threw an error: ${err}`);
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if result {
|
||||||
|
print(`Test ${test.name} PASSED`);
|
||||||
|
passed += 1;
|
||||||
|
} else {
|
||||||
|
print(`Test ${test.name} FAILED`);
|
||||||
|
failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(`\nTest summary: ${passed} passed, ${failed} failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests
|
||||||
|
run_tests();
|
@ -1,106 +0,0 @@
|
|||||||
// 04_buildah_operations.rhai
|
|
||||||
// Demonstrates container operations using SAL's buildah integration
|
|
||||||
// Note: This script requires buildah to be installed and may need root privileges
|
|
||||||
|
|
||||||
// Check if buildah is installed
|
|
||||||
let buildah_exists = which("buildah");
|
|
||||||
println(`Buildah exists: ${buildah_exists}`);
|
|
||||||
|
|
||||||
// Create a builder object
|
|
||||||
println("\nCreating a builder object:");
|
|
||||||
let container_name = "my-container-example";
|
|
||||||
|
|
||||||
// Create a new builder
|
|
||||||
let builder = bah_new(container_name, "alpine:latest");
|
|
||||||
|
|
||||||
// Reset the builder to remove any existing container
|
|
||||||
println("\nResetting the builder to start fresh:");
|
|
||||||
let reset_result = builder.reset();
|
|
||||||
println(`Reset result: ${reset_result}`);
|
|
||||||
|
|
||||||
// Create a new container after reset
|
|
||||||
println("\nCreating a new container after reset:");
|
|
||||||
builder = bah_new(container_name, "alpine:latest");
|
|
||||||
println(`Container created with ID: ${builder.container_id}`);
|
|
||||||
println(`Builder created with name: ${builder.name}, image: ${builder.image}`);
|
|
||||||
|
|
||||||
// List available images (only if buildah is installed)
|
|
||||||
println("\nListing available container images:");
|
|
||||||
// if ! buildah_exists != "" {
|
|
||||||
// //EXIT
|
|
||||||
// }
|
|
||||||
let images = builder.images();
|
|
||||||
println(`Found ${images.len()} images`);
|
|
||||||
|
|
||||||
// Print image details (limited to 3)
|
|
||||||
let count = 0;
|
|
||||||
for img in images {
|
|
||||||
if count >= 3 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
println(` - ID: ${img.id}, Name: ${img.name}, Created: ${img.created}`);
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Run a command in the container
|
|
||||||
println("\nRunning a command in the container:");
|
|
||||||
let run_result = builder.run("echo 'Hello from container'");
|
|
||||||
println(`Command output: ${run_result.stdout}`);
|
|
||||||
|
|
||||||
//Add a file to the container
|
|
||||||
println("\nAdding a file to the container:");
|
|
||||||
let test_file = "test_file.txt";
|
|
||||||
// Create the test file using Rhai's file_write function
|
|
||||||
file_write(test_file, "Test content");
|
|
||||||
println(`Created test file: ${test_file}`);
|
|
||||||
println(`Created test file: ${test_file}`);
|
|
||||||
let add_result = builder.add(test_file, "/");
|
|
||||||
println(`Add result: ${add_result.success}`);
|
|
||||||
|
|
||||||
//Commit the container to create a new image
|
|
||||||
println("\nCommitting the container to create a new image:");
|
|
||||||
let commit_result = builder.commit("my-custom-image:latest");
|
|
||||||
println(`Commit result: ${commit_result.success}`);
|
|
||||||
|
|
||||||
//Remove the container
|
|
||||||
println("\nRemoving the container:");
|
|
||||||
let remove_result = builder.remove();
|
|
||||||
println(`Remove result: ${remove_result.success}`);
|
|
||||||
|
|
||||||
//Clean up the test file
|
|
||||||
delete(test_file);
|
|
||||||
|
|
||||||
// Demonstrate static methods
|
|
||||||
println("\nDemonstrating static methods:");
|
|
||||||
println("Building an image from a Dockerfile:");
|
|
||||||
let build_result = builder.build("example-image:latest", ".", "example_Dockerfile", "chroot");
|
|
||||||
println(`Build result: ${build_result.success}`);
|
|
||||||
|
|
||||||
// Pull an image
|
|
||||||
println("\nPulling an image:");
|
|
||||||
let pull_result = builder.image_pull("alpine:latest", true);
|
|
||||||
println(`Pull result: ${pull_result.success}`);
|
|
||||||
|
|
||||||
// Skip commit options demonstration since we removed the legacy functions
|
|
||||||
println("\nSkipping commit options demonstration (legacy functions removed)");
|
|
||||||
|
|
||||||
// Demonstrate config method
|
|
||||||
println("\nDemonstrating config method:");
|
|
||||||
// Create a new container for config demonstration
|
|
||||||
println("Creating a new container for config demonstration:");
|
|
||||||
builder = bah_new("config-demo-container", "alpine:latest");
|
|
||||||
println(`Container created with ID: ${builder.container_id}`);
|
|
||||||
|
|
||||||
let config_options = #{
|
|
||||||
"author": "Rhai Example",
|
|
||||||
"cmd": "/bin/sh -c 'echo Hello from Buildah'"
|
|
||||||
};
|
|
||||||
let config_result = builder.config(config_options);
|
|
||||||
println(`Config result: ${config_result.success}`);
|
|
||||||
|
|
||||||
// Clean up the container
|
|
||||||
println("Removing the config demo container:");
|
|
||||||
builder.remove();
|
|
||||||
|
|
||||||
|
|
||||||
"Buildah operations script completed successfully!"
|
|
@ -1,164 +0,0 @@
|
|||||||
// Simplified test script for Git module functions
|
|
||||||
|
|
||||||
// Ensure test directory exists using a bash script
|
|
||||||
fn ensure_test_dir() {
|
|
||||||
print("Ensuring test directory exists at /tmp/code");
|
|
||||||
|
|
||||||
// Create a bash script to set up the test environment
|
|
||||||
let setup_script = `#!/bin/bash -ex
|
|
||||||
rm -rf /tmp/code
|
|
||||||
mkdir -p /tmp/code
|
|
||||||
cd /tmp/code
|
|
||||||
|
|
||||||
mkdir -p myserver.com/myaccount/repogreen
|
|
||||||
mkdir -p myserver.com/myaccount/repored
|
|
||||||
|
|
||||||
cd myserver.com/myaccount/repogreen
|
|
||||||
git init
|
|
||||||
echo 'Initial test file' > test.txt
|
|
||||||
git add test.txt
|
|
||||||
git config --local user.email 'test@example.com'
|
|
||||||
git config --local user.name 'Test User'
|
|
||||||
git commit -m 'Initial commit'
|
|
||||||
|
|
||||||
cd myserver.com/myaccount/repored
|
|
||||||
git init
|
|
||||||
echo 'Initial test file' > test2.txt
|
|
||||||
git add test2.txt
|
|
||||||
git config --local user.email 'test@example.com'
|
|
||||||
git config --local user.name 'Test User'
|
|
||||||
git commit -m 'Initial commit'
|
|
||||||
|
|
||||||
//now we have 2 repos
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Run the setup script
|
|
||||||
let result = run(setup_script);
|
|
||||||
if !result.success {
|
|
||||||
print("Failed to set up test directory");
|
|
||||||
print(`Error: ${result.stderr}`);
|
|
||||||
throw "Test setup failed";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test GitTree creation
|
|
||||||
fn test_git_tree_creation() {
|
|
||||||
print("\n=== Testing GitTree creation ===");
|
|
||||||
let git_tree = gittree_new("/tmp/code");
|
|
||||||
print(`Created GitTree with base path: /tmp/code`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test GitTree list method
|
|
||||||
fn test_git_tree_list() {
|
|
||||||
print("\n=== Testing GitTree list method ===");
|
|
||||||
let git_tree = gittree_new("/tmp/code");
|
|
||||||
let repos = git_tree.list();
|
|
||||||
|
|
||||||
print(`Found ${repos.len()} repositories`);
|
|
||||||
|
|
||||||
// Print repositories
|
|
||||||
for repo in repos {
|
|
||||||
print(` - ${repo}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if repos.len() == 0 {
|
|
||||||
print("No repositories found, which is unexpected");
|
|
||||||
throw "No repositories found";
|
|
||||||
}
|
|
||||||
|
|
||||||
if repos.len() != 2 {
|
|
||||||
print("No enough repositories found, needs to be 2");
|
|
||||||
throw "No enough repositories found";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test GitTree find method
|
|
||||||
fn test_git_tree_find() {
|
|
||||||
print("\n=== Testing GitTree find method ===");
|
|
||||||
let git_tree = gittree_new("/tmp/code");
|
|
||||||
|
|
||||||
// Search for repositories with "code" in the name
|
|
||||||
let search_pattern = "myaccount/repo"; //we need to check if we need *, would be better not
|
|
||||||
print(`Searching for repositories matching pattern: ${search_pattern}`);
|
|
||||||
let matching = git_tree.find(search_pattern);
|
|
||||||
|
|
||||||
print(`Found ${matching.len()} matching repositories`);
|
|
||||||
for repo in matching {
|
|
||||||
print(` - ${repo}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if matching.len() == 0 {
|
|
||||||
print("No matching repositories found, which is unexpected");
|
|
||||||
throw "No matching repositories found";
|
|
||||||
}
|
|
||||||
if repos.len() != 2 {
|
|
||||||
print("No enough repositories found, needs to be 2");
|
|
||||||
throw "No enough repositories found";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test GitRepo operations
|
|
||||||
fn test_git_repo_operations() {
|
|
||||||
print("\n=== Testing GitRepo operations ===");
|
|
||||||
let git_tree = gittree_new("/tmp/code");
|
|
||||||
let repos = git_tree.list();
|
|
||||||
|
|
||||||
if repos.len() == 0 {
|
|
||||||
print("No repositories found, which is unexpected");
|
|
||||||
throw "No repositories found";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the first repo
|
|
||||||
let repo_path = repos[0];
|
|
||||||
print(`Testing operations on repository: ${repo_path}`);
|
|
||||||
|
|
||||||
// Get GitRepo object
|
|
||||||
let git_repos = git_tree.get(repo_path);
|
|
||||||
if git_repos.len() == 0 {
|
|
||||||
print("Failed to get GitRepo object");
|
|
||||||
throw "Failed to get GitRepo object";
|
|
||||||
}
|
|
||||||
|
|
||||||
let git_repo = git_repos[0];
|
|
||||||
|
|
||||||
// Test has_changes method
|
|
||||||
print("Testing has_changes method");
|
|
||||||
let has_changes = git_repo.has_changes();
|
|
||||||
print(`Repository has changes: ${has_changes}`);
|
|
||||||
|
|
||||||
// Create a change to test
|
|
||||||
print("Creating a change to test");
|
|
||||||
file_write("/tmp/code/test2.txt", "Another test file");
|
|
||||||
|
|
||||||
// Check if changes are detected
|
|
||||||
let has_changes_after = git_repo.has_changes();
|
|
||||||
print(`Repository has changes after modification: ${has_changes_after}`);
|
|
||||||
|
|
||||||
if !has_changes_after {
|
|
||||||
print("Changes not detected, which is unexpected");
|
|
||||||
throw "Changes not detected";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up the change
|
|
||||||
delete("/tmp/code/test2.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
fn run_all_tests() {
|
|
||||||
print("Starting Git module tests...");
|
|
||||||
|
|
||||||
// Ensure test directory exists
|
|
||||||
ensure_test_dir();
|
|
||||||
|
|
||||||
// Run tests
|
|
||||||
test_git_tree_creation();
|
|
||||||
test_git_tree_list();
|
|
||||||
test_git_tree_find();
|
|
||||||
test_git_repo_operations();
|
|
||||||
|
|
||||||
print("\nAll tests completed successfully!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
run_all_tests();
|
|
@ -1,33 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
// Create a bash script to set up the test environment
|
|
||||||
let setup_script = `
|
|
||||||
rm -rf /tmp/code
|
|
||||||
mkdir -p /tmp/code
|
|
||||||
cd /tmp/code
|
|
||||||
|
|
||||||
mkdir -p myserver.com/myaccount/repogreen
|
|
||||||
mkdir -p myserver.com/myaccount/repored
|
|
||||||
|
|
||||||
cd myserver.com/myaccount/repogreen
|
|
||||||
git init
|
|
||||||
echo 'Initial test file' > test.txt
|
|
||||||
git add test.txt
|
|
||||||
git config --local user.email 'test@example.com'
|
|
||||||
git config --local user.name 'Test User'
|
|
||||||
git commit -m 'Initial commit'
|
|
||||||
|
|
||||||
cd myserver.com/myaccount/repored
|
|
||||||
git init
|
|
||||||
echo 'Initial test file' > test2.txt
|
|
||||||
git add test2.txt
|
|
||||||
git config --local user.email 'test@example.com'
|
|
||||||
git config --local user.name 'Test User'
|
|
||||||
git commit -m 'Initial commit'
|
|
||||||
|
|
||||||
//now we have 2 repos
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Run the setup script
|
|
||||||
let result = run(setup_script);
|
|
42
src/test_git.rhai
Normal file
42
src/test_git.rhai
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Simple test script for Git module functions
|
||||||
|
|
||||||
|
// Print a header
|
||||||
|
print("=== Testing Git Module Functions ===\n");
|
||||||
|
|
||||||
|
// Test git_list function
|
||||||
|
print("Listing git repositories...");
|
||||||
|
let repos = git_list();
|
||||||
|
print(`Found ${repos.len()} repositories`);
|
||||||
|
|
||||||
|
// Print the first few repositories
|
||||||
|
if repos.len() > 0 {
|
||||||
|
print("First few repositories:");
|
||||||
|
let count = if repos.len() > 3 { 3 } else { repos.len() };
|
||||||
|
for i in range(0, count) {
|
||||||
|
print(` - ${repos[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test find_matching_repos function
|
||||||
|
if repos.len() > 0 {
|
||||||
|
print("\nTesting repository search...");
|
||||||
|
// Extract a part of the first repo name to search for
|
||||||
|
let repo_path = repos[0];
|
||||||
|
let parts = repo_path.split("/");
|
||||||
|
let repo_name = parts[parts.len() - 1];
|
||||||
|
|
||||||
|
print(`Searching for repositories containing "${repo_name}"`);
|
||||||
|
let matching = find_matching_repos(repo_name);
|
||||||
|
|
||||||
|
print(`Found ${matching.len()} matching repositories`);
|
||||||
|
for repo in matching {
|
||||||
|
print(` - ${repo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a repository has changes
|
||||||
|
print("\nChecking for changes in repository...");
|
||||||
|
let has_changes = git_has_changes(repo_path);
|
||||||
|
print(`Repository ${repo_path} has changes: ${has_changes}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n=== Git Module Test Complete ===");
|
@ -1,458 +0,0 @@
|
|||||||
use crate::process::CommandResult;
|
|
||||||
use crate::virt::buildah::{execute_buildah_command, BuildahError, Image};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Builder struct for buildah operations
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Builder {
|
|
||||||
/// Name of the container
|
|
||||||
name: String,
|
|
||||||
/// Container ID
|
|
||||||
container_id: Option<String>,
|
|
||||||
/// Base image
|
|
||||||
image: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Builder {
|
|
||||||
/// Create a new builder with a container from the specified image
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - Name for the container
|
|
||||||
/// * `image` - Image to create the container from
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<Self, BuildahError>` - Builder instance or error
|
|
||||||
pub fn new(name: &str, image: &str) -> Result<Self, BuildahError> {
|
|
||||||
// Try to create a new container
|
|
||||||
let result = execute_buildah_command(&["from", "--name", name, image]);
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(success_result) => {
|
|
||||||
// Container created successfully
|
|
||||||
let container_id = success_result.stdout.trim().to_string();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
container_id: Some(container_id),
|
|
||||||
image: image.to_string(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Err(BuildahError::CommandFailed(error_msg)) => {
|
|
||||||
// Check if the error is because the container already exists
|
|
||||||
if error_msg.contains("that name is already in use") {
|
|
||||||
// Extract the container ID from the error message
|
|
||||||
// Error format: "the container name "name" is already in use by container_id. You have to remove that container to be able to reuse that name: that name is already in use"
|
|
||||||
let container_id = error_msg
|
|
||||||
.split("already in use by ")
|
|
||||||
.nth(1)
|
|
||||||
.and_then(|s| s.split('.').next())
|
|
||||||
.unwrap_or("")
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if !container_id.is_empty() {
|
|
||||||
// Container already exists, continue with it
|
|
||||||
Ok(Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
container_id: Some(container_id),
|
|
||||||
image: image.to_string(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Couldn't extract container ID
|
|
||||||
Err(BuildahError::Other("Failed to extract container ID from error message".to_string()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Other command failure
|
|
||||||
Err(BuildahError::CommandFailed(error_msg))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
// Other error
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the container ID
|
|
||||||
pub fn container_id(&self) -> Option<&String> {
|
|
||||||
self.container_id.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the container name
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the base image
|
|
||||||
pub fn image(&self) -> &str {
|
|
||||||
&self.image
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a command in the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `command` - The command to run
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn run(&self, command: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["run", container_id, "sh", "-c", command])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a command in the container with specified isolation
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `command` - The command to run
|
|
||||||
/// * `isolation` - Isolation method (e.g., "chroot", "rootless", "oci")
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn run_with_isolation(&self, command: &str, isolation: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["run", "--isolation", isolation, container_id, "sh", "-c", command])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copy files into the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `source` - Source path
|
|
||||||
/// * `dest` - Destination path in the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["copy", container_id, source, dest])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add files into the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `source` - Source path
|
|
||||||
/// * `dest` - Destination path in the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn add(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["add", container_id, source, dest])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commit the container to an image
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `image_name` - Name for the new image
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn commit(&self, image_name: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["commit", container_id, image_name])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn remove(&self) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_buildah_command(&["rm", container_id])
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset the builder by removing the container and clearing the container_id
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<(), BuildahError>` - Success or error
|
|
||||||
pub fn reset(&mut self) -> Result<(), BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
// Try to remove the container
|
|
||||||
let result = execute_buildah_command(&["rm", container_id]);
|
|
||||||
|
|
||||||
// Clear the container_id regardless of whether the removal succeeded
|
|
||||||
self.container_id = None;
|
|
||||||
|
|
||||||
// Return the result of the removal operation
|
|
||||||
match result {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No container to remove
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configure container metadata
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `options` - Map of configuration options
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn config(&self, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
let mut args_owned: Vec<String> = Vec::new();
|
|
||||||
args_owned.push("config".to_string());
|
|
||||||
|
|
||||||
// Process options map
|
|
||||||
for (key, value) in options.iter() {
|
|
||||||
let option_name = format!("--{}", key);
|
|
||||||
args_owned.push(option_name);
|
|
||||||
args_owned.push(value.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
args_owned.push(container_id.clone());
|
|
||||||
|
|
||||||
// Convert Vec<String> to Vec<&str> for execute_buildah_command
|
|
||||||
let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List images in local storage
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<Vec<Image>, BuildahError>` - List of images or error
|
|
||||||
pub fn images() -> Result<Vec<Image>, BuildahError> {
|
|
||||||
let result = execute_buildah_command(&["images", "--json"])?;
|
|
||||||
|
|
||||||
// Try to parse the JSON output
|
|
||||||
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
|
|
||||||
Ok(json) => {
|
|
||||||
if let serde_json::Value::Array(images_json) = json {
|
|
||||||
let mut images = Vec::new();
|
|
||||||
|
|
||||||
for image_json in images_json {
|
|
||||||
// Extract image ID
|
|
||||||
let id = match image_json.get("id").and_then(|v| v.as_str()) {
|
|
||||||
Some(id) => id.to_string(),
|
|
||||||
None => return Err(BuildahError::ConversionError("Missing image ID".to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract image names
|
|
||||||
let names = match image_json.get("names").and_then(|v| v.as_array()) {
|
|
||||||
Some(names_array) => {
|
|
||||||
let mut names_vec = Vec::new();
|
|
||||||
for name_value in names_array {
|
|
||||||
if let Some(name_str) = name_value.as_str() {
|
|
||||||
names_vec.push(name_str.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
names_vec
|
|
||||||
},
|
|
||||||
None => Vec::new(), // Empty vector if no names found
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract image size
|
|
||||||
let size = match image_json.get("size").and_then(|v| v.as_str()) {
|
|
||||||
Some(size) => size.to_string(),
|
|
||||||
None => "Unknown".to_string(), // Default value if size not found
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract creation timestamp
|
|
||||||
let created = match image_json.get("created").and_then(|v| v.as_str()) {
|
|
||||||
Some(created) => created.to_string(),
|
|
||||||
None => "Unknown".to_string(), // Default value if created not found
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create Image struct and add to vector
|
|
||||||
images.push(Image {
|
|
||||||
id,
|
|
||||||
names,
|
|
||||||
size,
|
|
||||||
created,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(images)
|
|
||||||
} else {
|
|
||||||
Err(BuildahError::JsonParseError("Expected JSON array".to_string()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
Err(BuildahError::JsonParseError(format!("Failed to parse image list JSON: {}", e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove an image
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `image` - Image ID or name
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn image_remove(image: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
execute_buildah_command(&["rmi", image])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pull an image from a registry
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `image` - Image name
|
|
||||||
/// * `tls_verify` - Whether to verify TLS
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
|
|
||||||
let mut args = vec!["pull"];
|
|
||||||
|
|
||||||
if !tls_verify {
|
|
||||||
args.push("--tls-verify=false");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(image);
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push an image to a registry
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `image` - Image name
|
|
||||||
/// * `destination` - Destination registry
|
|
||||||
/// * `tls_verify` - Whether to verify TLS
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
|
|
||||||
let mut args = vec!["push"];
|
|
||||||
|
|
||||||
if !tls_verify {
|
|
||||||
args.push("--tls-verify=false");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(image);
|
|
||||||
args.push(destination);
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tag an image
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `image` - Image ID or name
|
|
||||||
/// * `new_name` - New tag for the image
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, BuildahError> {
|
|
||||||
execute_buildah_command(&["tag", image, new_name])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commit a container to an image with advanced options
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `container` - Container ID or name
|
|
||||||
/// * `image_name` - Name for the new image
|
|
||||||
/// * `format` - Optional format (oci or docker)
|
|
||||||
/// * `squash` - Whether to squash layers
|
|
||||||
/// * `rm` - Whether to remove the container after commit
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn image_commit(container: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError> {
|
|
||||||
let mut args = vec!["commit"];
|
|
||||||
|
|
||||||
if let Some(format_str) = format {
|
|
||||||
args.push("--format");
|
|
||||||
args.push(format_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
if squash {
|
|
||||||
args.push("--squash");
|
|
||||||
}
|
|
||||||
|
|
||||||
if rm {
|
|
||||||
args.push("--rm");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(container);
|
|
||||||
args.push(image_name);
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build an image from a Containerfile/Dockerfile
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `tag` - Optional tag for the image
|
|
||||||
/// * `context_dir` - Directory containing the Containerfile/Dockerfile
|
|
||||||
/// * `file` - Path to the Containerfile/Dockerfile
|
|
||||||
/// * `isolation` - Optional isolation method
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
|
||||||
pub fn build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError> {
|
|
||||||
let mut args = Vec::new();
|
|
||||||
args.push("build");
|
|
||||||
|
|
||||||
if let Some(tag_value) = tag {
|
|
||||||
args.push("-t");
|
|
||||||
args.push(tag_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(isolation_value) = isolation {
|
|
||||||
args.push("--isolation");
|
|
||||||
args.push(isolation_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("-f");
|
|
||||||
args.push(file);
|
|
||||||
|
|
||||||
args.push(context_dir);
|
|
||||||
|
|
||||||
execute_buildah_command(&args)
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,7 +11,6 @@ mod tests {
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref LAST_COMMAND: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
static ref LAST_COMMAND: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||||
static ref SHOULD_FAIL: Mutex<bool> = Mutex::new(false);
|
static ref SHOULD_FAIL: Mutex<bool> = Mutex::new(false);
|
||||||
static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); // Add a mutex for test synchronization
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_test_state() {
|
fn reset_test_state() {
|
||||||
@ -118,7 +117,6 @@ mod tests {
|
|||||||
// Tests for each function
|
// Tests for each function
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_function() {
|
fn test_from_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
let image = "alpine:latest";
|
let image = "alpine:latest";
|
||||||
@ -131,7 +129,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_run_function() {
|
fn test_run_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
let container = "my-container";
|
let container = "my-container";
|
||||||
@ -146,7 +143,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bah_run_with_isolation_function() {
|
fn test_bah_run_with_isolation_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
let container = "my-container";
|
let container = "my-container";
|
||||||
@ -161,7 +157,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bah_copy_function() {
|
fn test_bah_copy_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
let container = "my-container";
|
let container = "my-container";
|
||||||
@ -176,7 +171,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bah_add_function() {
|
fn test_bah_add_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
let container = "my-container";
|
let container = "my-container";
|
||||||
@ -191,7 +185,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bah_commit_function() {
|
fn test_bah_commit_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
let container = "my-container";
|
let container = "my-container";
|
||||||
@ -205,7 +198,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bah_remove_function() {
|
fn test_bah_remove_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
let container = "my-container";
|
let container = "my-container";
|
||||||
@ -218,7 +210,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bah_list_function() {
|
fn test_bah_list_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
let result = test_bah_list();
|
let result = test_bah_list();
|
||||||
@ -230,7 +221,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bah_build_function() {
|
fn test_bah_build_function() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
// Test with tag, context directory, file, and no isolation
|
// Test with tag, context directory, file, and no isolation
|
||||||
@ -239,16 +229,12 @@ mod tests {
|
|||||||
let cmd = get_last_command();
|
let cmd = get_last_command();
|
||||||
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "-f", "Dockerfile", "."]);
|
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "-f", "Dockerfile", "."]);
|
||||||
|
|
||||||
reset_test_state(); // Reset state between sub-tests
|
|
||||||
|
|
||||||
// Test with tag, context directory, file, and isolation
|
// Test with tag, context directory, file, and isolation
|
||||||
let result = test_bah_build(Some("my-app:latest"), ".", "Dockerfile.custom", Some("chroot"));
|
let result = test_bah_build(Some("my-app:latest"), ".", "Dockerfile.custom", Some("chroot"));
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
let cmd = get_last_command();
|
let cmd = get_last_command();
|
||||||
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "--isolation", "chroot", "-f", "Dockerfile.custom", "."]);
|
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "--isolation", "chroot", "-f", "Dockerfile.custom", "."]);
|
||||||
|
|
||||||
reset_test_state(); // Reset state between sub-tests
|
|
||||||
|
|
||||||
// Test with just context directory and file
|
// Test with just context directory and file
|
||||||
let result = test_bah_build(None, ".", "Dockerfile", None);
|
let result = test_bah_build(None, ".", "Dockerfile", None);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
@ -258,7 +244,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_error_handling() {
|
fn test_error_handling() {
|
||||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
set_should_fail(true);
|
set_should_fail(true);
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
mod containers;
|
mod containers;
|
||||||
mod images;
|
mod images;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod builder;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod containers_test;
|
mod containers_test;
|
||||||
|
|
||||||
@ -44,12 +43,6 @@ impl Error for BuildahError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Re-export the Builder
|
|
||||||
pub use builder::Builder;
|
|
||||||
|
|
||||||
// Re-export existing functions for backward compatibility
|
|
||||||
#[deprecated(since = "0.2.0", note = "Use Builder::new() instead")]
|
|
||||||
pub use containers::*;
|
pub use containers::*;
|
||||||
#[deprecated(since = "0.2.0", note = "Use Builder methods instead")]
|
|
||||||
pub use images::*;
|
pub use images::*;
|
||||||
pub use cmd::*;
|
pub use cmd::*;
|
@ -1,374 +0,0 @@
|
|||||||
# Container API for nerdctl
|
|
||||||
|
|
||||||
This module provides a Rust API for managing containers using nerdctl, a Docker-compatible CLI for containerd.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Container API is designed with a builder pattern to make it easy to create and manage containers. It provides a fluent interface for configuring container options and performing operations on containers.
|
|
||||||
|
|
||||||
## Key Components
|
|
||||||
|
|
||||||
- `Container`: The main struct representing a container
|
|
||||||
- `HealthCheck`: Configuration for container health checks
|
|
||||||
- `ContainerStatus`: Information about a container's status
|
|
||||||
- `ResourceUsage`: Information about a container's resource usage
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- nerdctl must be installed on your system
|
|
||||||
- containerd must be running
|
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
Add the following to your `Cargo.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
sal = { path = "/path/to/sal" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Then import the Container API in your Rust code:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use sal::virt::nerdctl::Container;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Getting a Container by Name
|
|
||||||
|
|
||||||
You can get a reference to an existing container by name:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use sal::virt::nerdctl::Container;
|
|
||||||
|
|
||||||
// Get a container by name (if it exists)
|
|
||||||
match Container::new("existing-container") {
|
|
||||||
Ok(container) => {
|
|
||||||
if container.container_id.is_some() {
|
|
||||||
println!("Found container with ID: {}", container.container_id.unwrap());
|
|
||||||
|
|
||||||
// Perform operations on the existing container
|
|
||||||
let status = container.status()?;
|
|
||||||
println!("Container status: {}", status.status);
|
|
||||||
} else {
|
|
||||||
println!("Container exists but has no ID");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error getting container: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Creating a Container
|
|
||||||
|
|
||||||
You can create a new container from an image using the builder pattern:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use sal::virt::nerdctl::Container;
|
|
||||||
|
|
||||||
// Create a container from an image
|
|
||||||
let container = Container::from_image("my-nginx", "nginx:latest")?
|
|
||||||
.with_port("8080:80")
|
|
||||||
.with_env("NGINX_HOST", "example.com")
|
|
||||||
.with_volume("/tmp/nginx:/usr/share/nginx/html")
|
|
||||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
|
||||||
.with_detach(true)
|
|
||||||
.build()?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container Operations
|
|
||||||
|
|
||||||
Once you have a container, you can perform various operations on it:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Execute a command in the container
|
|
||||||
let result = container.exec("echo 'Hello from container'")?;
|
|
||||||
println!("Command output: {}", result.stdout);
|
|
||||||
|
|
||||||
// Get container status
|
|
||||||
let status = container.status()?;
|
|
||||||
println!("Container status: {}", status.status);
|
|
||||||
|
|
||||||
// Get resource usage
|
|
||||||
let resources = container.resources()?;
|
|
||||||
println!("CPU usage: {}", resources.cpu_usage);
|
|
||||||
println!("Memory usage: {}", resources.memory_usage);
|
|
||||||
|
|
||||||
// Stop and remove the container
|
|
||||||
container.stop()?;
|
|
||||||
container.remove()?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Container Configuration Options
|
|
||||||
|
|
||||||
The Container API supports a wide range of configuration options through its builder pattern:
|
|
||||||
|
|
||||||
### Ports
|
|
||||||
|
|
||||||
Map container ports to host ports:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Map a single port
|
|
||||||
.with_port("8080:80")
|
|
||||||
|
|
||||||
// Map multiple ports
|
|
||||||
.with_ports(&["8080:80", "8443:443"])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Volumes
|
|
||||||
|
|
||||||
Mount host directories or volumes in the container:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Mount a single volume
|
|
||||||
.with_volume("/host/path:/container/path")
|
|
||||||
|
|
||||||
// Mount multiple volumes
|
|
||||||
.with_volumes(&["/host/path1:/container/path1", "/host/path2:/container/path2"])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Set environment variables in the container:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Set a single environment variable
|
|
||||||
.with_env("KEY", "value")
|
|
||||||
|
|
||||||
// Set multiple environment variables
|
|
||||||
let mut env_map = HashMap::new();
|
|
||||||
env_map.insert("KEY1", "value1");
|
|
||||||
env_map.insert("KEY2", "value2");
|
|
||||||
.with_envs(&env_map)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Network Configuration
|
|
||||||
|
|
||||||
Configure container networking:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Set the network
|
|
||||||
.with_network("bridge")
|
|
||||||
|
|
||||||
// Add a network alias
|
|
||||||
.with_network_alias("my-container")
|
|
||||||
|
|
||||||
// Add multiple network aliases
|
|
||||||
.with_network_aliases(&["alias1", "alias2"])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Limits
|
|
||||||
|
|
||||||
Set CPU and memory limits:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Set CPU limit (e.g., 0.5 for half a CPU, 2 for 2 CPUs)
|
|
||||||
.with_cpu_limit("0.5")
|
|
||||||
|
|
||||||
// Set memory limit (e.g., 512m for 512MB, 1g for 1GB)
|
|
||||||
.with_memory_limit("512m")
|
|
||||||
|
|
||||||
// Set memory swap limit
|
|
||||||
.with_memory_swap_limit("1g")
|
|
||||||
|
|
||||||
// Set CPU shares (relative weight)
|
|
||||||
.with_cpu_shares("1024")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
|
|
||||||
Configure container health checks:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Simple health check
|
|
||||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
|
||||||
|
|
||||||
// Health check with custom options
|
|
||||||
.with_health_check_options(
|
|
||||||
"curl -f http://localhost/ || exit 1", // Command
|
|
||||||
Some("30s"), // Interval
|
|
||||||
Some("10s"), // Timeout
|
|
||||||
Some(3), // Retries
|
|
||||||
Some("5s") // Start period
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Other Options
|
|
||||||
|
|
||||||
Other container configuration options:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Set restart policy
|
|
||||||
.with_restart_policy("always") // Options: no, always, on-failure, unless-stopped
|
|
||||||
|
|
||||||
// Set snapshotter
|
|
||||||
.with_snapshotter("native") // Options: native, fuse-overlayfs, etc.
|
|
||||||
|
|
||||||
// Set detach mode
|
|
||||||
.with_detach(true) // Run in detached mode
|
|
||||||
```
|
|
||||||
|
|
||||||
## Container Operations
|
|
||||||
|
|
||||||
Once a container is created, you can perform various operations on it:
|
|
||||||
|
|
||||||
### Basic Operations
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Start the container
|
|
||||||
container.start()?;
|
|
||||||
|
|
||||||
// Stop the container
|
|
||||||
container.stop()?;
|
|
||||||
|
|
||||||
// Remove the container
|
|
||||||
container.remove()?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Command Execution
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Execute a command in the container
|
|
||||||
let result = container.exec("echo 'Hello from container'")?;
|
|
||||||
println!("Command output: {}", result.stdout);
|
|
||||||
```
|
|
||||||
|
|
||||||
### File Operations
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Copy files between the container and host
|
|
||||||
container.copy("container_name:/path/in/container", "/path/on/host")?;
|
|
||||||
container.copy("/path/on/host", "container_name:/path/in/container")?;
|
|
||||||
|
|
||||||
// Export the container to a tarball
|
|
||||||
container.export("/path/to/export.tar")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image Operations
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Commit the container to an image
|
|
||||||
container.commit("my-custom-image:latest")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status and Monitoring
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Get container status
|
|
||||||
let status = container.status()?;
|
|
||||||
println!("Container state: {}", status.state);
|
|
||||||
println!("Container status: {}", status.status);
|
|
||||||
println!("Created: {}", status.created);
|
|
||||||
println!("Started: {}", status.started);
|
|
||||||
|
|
||||||
// Get health status
|
|
||||||
let health_status = container.health_status()?;
|
|
||||||
println!("Health status: {}", health_status);
|
|
||||||
|
|
||||||
// Get resource usage
|
|
||||||
let resources = container.resources()?;
|
|
||||||
println!("CPU usage: {}", resources.cpu_usage);
|
|
||||||
println!("Memory usage: {}", resources.memory_usage);
|
|
||||||
println!("Memory limit: {}", resources.memory_limit);
|
|
||||||
println!("Memory percentage: {}", resources.memory_percentage);
|
|
||||||
println!("Network I/O: {} / {}", resources.network_input, resources.network_output);
|
|
||||||
println!("Block I/O: {} / {}", resources.block_input, resources.block_output);
|
|
||||||
println!("PIDs: {}", resources.pids);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
The Container API uses a custom error type `NerdctlError` that can be one of the following:
|
|
||||||
|
|
||||||
- `CommandExecutionFailed`: The nerdctl command failed to execute
|
|
||||||
- `CommandFailed`: The nerdctl command executed but returned an error
|
|
||||||
- `JsonParseError`: Failed to parse JSON output
|
|
||||||
- `ConversionError`: Failed to convert data
|
|
||||||
- `Other`: Generic error
|
|
||||||
|
|
||||||
Example error handling:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
match Container::new("non-existent-container") {
|
|
||||||
Ok(container) => {
|
|
||||||
// Container exists
|
|
||||||
println!("Container found");
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
match e {
|
|
||||||
NerdctlError::CommandExecutionFailed(io_error) => {
|
|
||||||
println!("Failed to execute nerdctl command: {}", io_error);
|
|
||||||
},
|
|
||||||
NerdctlError::CommandFailed(error_msg) => {
|
|
||||||
println!("nerdctl command failed: {}", error_msg);
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
println!("Other error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
The Container API is implemented in several modules:
|
|
||||||
|
|
||||||
- `container_types.rs`: Contains the struct definitions
|
|
||||||
- `container.rs`: Contains the main Container implementation
|
|
||||||
- `container_builder.rs`: Contains the builder pattern methods
|
|
||||||
- `container_operations.rs`: Contains the container operations
|
|
||||||
- `health_check.rs`: Contains the HealthCheck implementation
|
|
||||||
|
|
||||||
This modular approach makes the code more maintainable and easier to understand.
|
|
||||||
|
|
||||||
## Complete Example
|
|
||||||
|
|
||||||
Here's a complete example that demonstrates the Container API:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use std::error::Error;
|
|
||||||
use sal::virt::nerdctl::Container;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
// Create a container from an image
|
|
||||||
println!("Creating container from image...");
|
|
||||||
let container = Container::from_image("my-nginx", "nginx:latest")?
|
|
||||||
.with_port("8080:80")
|
|
||||||
.with_env("NGINX_HOST", "example.com")
|
|
||||||
.with_volume("/tmp/nginx:/usr/share/nginx/html")
|
|
||||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
|
||||||
.with_detach(true)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
println!("Container created successfully");
|
|
||||||
|
|
||||||
// Execute a command in the container
|
|
||||||
println!("Executing command in container...");
|
|
||||||
let result = container.exec("echo 'Hello from container'")?;
|
|
||||||
println!("Command output: {}", result.stdout);
|
|
||||||
|
|
||||||
// Get container status
|
|
||||||
println!("Getting container status...");
|
|
||||||
let status = container.status()?;
|
|
||||||
println!("Container status: {}", status.status);
|
|
||||||
|
|
||||||
// Get resource usage
|
|
||||||
println!("Getting resource usage...");
|
|
||||||
let resources = container.resources()?;
|
|
||||||
println!("CPU usage: {}", resources.cpu_usage);
|
|
||||||
println!("Memory usage: {}", resources.memory_usage);
|
|
||||||
|
|
||||||
// Stop and remove the container
|
|
||||||
println!("Stopping and removing container...");
|
|
||||||
container.stop()?;
|
|
||||||
container.remove()?;
|
|
||||||
|
|
||||||
println!("Container stopped and removed");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container.rs
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::process::CommandResult;
|
|
||||||
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
|
|
||||||
use super::container_types::{Container, HealthCheck, ContainerStatus, ResourceUsage};
|
|
||||||
|
|
||||||
impl Container {
|
|
||||||
/// Create a new container reference with the given name
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - Name for the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<Self, NerdctlError>` - Container instance or error
|
|
||||||
pub fn new(name: &str) -> Result<Self, NerdctlError> {
|
|
||||||
// Check if container exists
|
|
||||||
let result = execute_nerdctl_command(&["ps", "-a", "--format", "{{.Names}} {{.ID}}"])?;
|
|
||||||
|
|
||||||
// Look for the container name in the output
|
|
||||||
let container_id = result.stdout.lines()
|
|
||||||
.filter_map(|line| {
|
|
||||||
if line.starts_with(&format!("{} ", name)) {
|
|
||||||
Some(line.split_whitespace().nth(1)?.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.next();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
container_id,
|
|
||||||
image: None,
|
|
||||||
config: HashMap::new(),
|
|
||||||
ports: Vec::new(),
|
|
||||||
volumes: Vec::new(),
|
|
||||||
env_vars: HashMap::new(),
|
|
||||||
network: None,
|
|
||||||
network_aliases: Vec::new(),
|
|
||||||
cpu_limit: None,
|
|
||||||
memory_limit: None,
|
|
||||||
memory_swap_limit: None,
|
|
||||||
cpu_shares: None,
|
|
||||||
restart_policy: None,
|
|
||||||
health_check: None,
|
|
||||||
detach: false,
|
|
||||||
snapshotter: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a container from an image
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - Name for the container
|
|
||||||
/// * `image` - Image to create the container from
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<Self, NerdctlError>` - Container instance or error
|
|
||||||
pub fn from_image(name: &str, image: &str) -> Result<Self, NerdctlError> {
|
|
||||||
let mut container = Self::new(name)?;
|
|
||||||
container.image = Some(image.to_string());
|
|
||||||
Ok(container)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,460 +0,0 @@
|
|||||||
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_builder.rs
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
|
|
||||||
use super::container_types::{Container, HealthCheck};
|
|
||||||
|
|
||||||
impl Container {
|
|
||||||
/// Add a port mapping
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `port` - Port mapping (e.g., "8080:80")
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_port(mut self, port: &str) -> Self {
|
|
||||||
self.ports.push(port.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add multiple port mappings
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `ports` - Array of port mappings (e.g., ["8080:80", "8443:443"])
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_ports(mut self, ports: &[&str]) -> Self {
|
|
||||||
for port in ports {
|
|
||||||
self.ports.push(port.to_string());
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a volume mount
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `volume` - Volume mount (e.g., "/host/path:/container/path")
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_volume(mut self, volume: &str) -> Self {
|
|
||||||
self.volumes.push(volume.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add multiple volume mounts
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `volumes` - Array of volume mounts (e.g., ["/host/path1:/container/path1", "/host/path2:/container/path2"])
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_volumes(mut self, volumes: &[&str]) -> Self {
|
|
||||||
for volume in volumes {
|
|
||||||
self.volumes.push(volume.to_string());
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an environment variable
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `key` - Environment variable name
|
|
||||||
/// * `value` - Environment variable value
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_env(mut self, key: &str, value: &str) -> Self {
|
|
||||||
self.env_vars.insert(key.to_string(), value.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add multiple environment variables
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `env_map` - Map of environment variable names to values
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_envs(mut self, env_map: &HashMap<&str, &str>) -> Self {
|
|
||||||
for (key, value) in env_map {
|
|
||||||
self.env_vars.insert(key.to_string(), value.to_string());
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the network for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `network` - Network name
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_network(mut self, network: &str) -> Self {
|
|
||||||
self.network = Some(network.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a network alias for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `alias` - Network alias
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_network_alias(mut self, alias: &str) -> Self {
|
|
||||||
self.network_aliases.push(alias.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add multiple network aliases for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `aliases` - Array of network aliases
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_network_aliases(mut self, aliases: &[&str]) -> Self {
|
|
||||||
for alias in aliases {
|
|
||||||
self.network_aliases.push(alias.to_string());
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set CPU limit for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cpus` - CPU limit (e.g., "0.5" for half a CPU, "2" for 2 CPUs)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_cpu_limit(mut self, cpus: &str) -> Self {
|
|
||||||
self.cpu_limit = Some(cpus.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set memory limit for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `memory` - Memory limit (e.g., "512m" for 512MB, "1g" for 1GB)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_memory_limit(mut self, memory: &str) -> Self {
|
|
||||||
self.memory_limit = Some(memory.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set memory swap limit for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `memory_swap` - Memory swap limit (e.g., "1g" for 1GB)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_memory_swap_limit(mut self, memory_swap: &str) -> Self {
|
|
||||||
self.memory_swap_limit = Some(memory_swap.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set CPU shares for the container (relative weight)
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `shares` - CPU shares (e.g., "1024" for default, "512" for half)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_cpu_shares(mut self, shares: &str) -> Self {
|
|
||||||
self.cpu_shares = Some(shares.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set restart policy for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `policy` - Restart policy (e.g., "no", "always", "on-failure", "unless-stopped")
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_restart_policy(mut self, policy: &str) -> Self {
|
|
||||||
self.restart_policy = Some(policy.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a simple health check for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cmd` - Command to run for health check (e.g., "curl -f http://localhost/ || exit 1")
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_health_check(mut self, cmd: &str) -> Self {
|
|
||||||
self.health_check = Some(HealthCheck {
|
|
||||||
cmd: cmd.to_string(),
|
|
||||||
interval: None,
|
|
||||||
timeout: None,
|
|
||||||
retries: None,
|
|
||||||
start_period: None,
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a health check with custom options for the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cmd` - Command to run for health check
|
|
||||||
/// * `interval` - Optional time between running the check (e.g., "30s", "1m")
|
|
||||||
/// * `timeout` - Optional maximum time to wait for a check to complete (e.g., "30s", "1m")
|
|
||||||
/// * `retries` - Optional number of consecutive failures needed to consider unhealthy
|
|
||||||
/// * `start_period` - Optional start period for the container to initialize before counting retries (e.g., "30s", "1m")
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_health_check_options(
|
|
||||||
mut self,
|
|
||||||
cmd: &str,
|
|
||||||
interval: Option<&str>,
|
|
||||||
timeout: Option<&str>,
|
|
||||||
retries: Option<u32>,
|
|
||||||
start_period: Option<&str>,
|
|
||||||
) -> Self {
|
|
||||||
let mut health_check = HealthCheck {
|
|
||||||
cmd: cmd.to_string(),
|
|
||||||
interval: None,
|
|
||||||
timeout: None,
|
|
||||||
retries: None,
|
|
||||||
start_period: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(interval_value) = interval {
|
|
||||||
health_check.interval = Some(interval_value.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(timeout_value) = timeout {
|
|
||||||
health_check.timeout = Some(timeout_value.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(retries_value) = retries {
|
|
||||||
health_check.retries = Some(retries_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(start_period_value) = start_period {
|
|
||||||
health_check.start_period = Some(start_period_value.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.health_check = Some(health_check);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the snapshotter
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `snapshotter` - Snapshotter to use
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_snapshotter(mut self, snapshotter: &str) -> Self {
|
|
||||||
self.snapshotter = Some(snapshotter.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set whether to run in detached mode
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `detach` - Whether to run in detached mode
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Self` - The container instance for method chaining
|
|
||||||
pub fn with_detach(mut self, detach: bool) -> Self {
|
|
||||||
self.detach = detach;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<Self, NerdctlError>` - Container instance or error
|
|
||||||
pub fn build(self) -> Result<Self, NerdctlError> {
|
|
||||||
// If container already exists, return it
|
|
||||||
if self.container_id.is_some() {
|
|
||||||
return Ok(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no image is specified, return an error
|
|
||||||
let image = match &self.image {
|
|
||||||
Some(img) => img,
|
|
||||||
None => return Err(NerdctlError::Other("No image specified for container creation".to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build the command arguments as strings
|
|
||||||
let mut args_strings = Vec::new();
|
|
||||||
args_strings.push("run".to_string());
|
|
||||||
|
|
||||||
if self.detach {
|
|
||||||
args_strings.push("-d".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
args_strings.push("--name".to_string());
|
|
||||||
args_strings.push(self.name.clone());
|
|
||||||
|
|
||||||
// Add port mappings
|
|
||||||
for port in &self.ports {
|
|
||||||
args_strings.push("-p".to_string());
|
|
||||||
args_strings.push(port.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add volume mounts
|
|
||||||
for volume in &self.volumes {
|
|
||||||
args_strings.push("-v".to_string());
|
|
||||||
args_strings.push(volume.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add environment variables
|
|
||||||
for (key, value) in &self.env_vars {
|
|
||||||
args_strings.push("-e".to_string());
|
|
||||||
args_strings.push(format!("{}={}", key, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add network configuration
|
|
||||||
if let Some(network) = &self.network {
|
|
||||||
args_strings.push("--network".to_string());
|
|
||||||
args_strings.push(network.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add network aliases
|
|
||||||
for alias in &self.network_aliases {
|
|
||||||
args_strings.push("--network-alias".to_string());
|
|
||||||
args_strings.push(alias.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add resource limits
|
|
||||||
if let Some(cpu_limit) = &self.cpu_limit {
|
|
||||||
args_strings.push("--cpus".to_string());
|
|
||||||
args_strings.push(cpu_limit.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(memory_limit) = &self.memory_limit {
|
|
||||||
args_strings.push("--memory".to_string());
|
|
||||||
args_strings.push(memory_limit.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(memory_swap_limit) = &self.memory_swap_limit {
|
|
||||||
args_strings.push("--memory-swap".to_string());
|
|
||||||
args_strings.push(memory_swap_limit.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cpu_shares) = &self.cpu_shares {
|
|
||||||
args_strings.push("--cpu-shares".to_string());
|
|
||||||
args_strings.push(cpu_shares.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add restart policy
|
|
||||||
if let Some(restart_policy) = &self.restart_policy {
|
|
||||||
args_strings.push("--restart".to_string());
|
|
||||||
args_strings.push(restart_policy.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add health check
|
|
||||||
if let Some(health_check) = &self.health_check {
|
|
||||||
args_strings.push("--health-cmd".to_string());
|
|
||||||
args_strings.push(health_check.cmd.clone());
|
|
||||||
|
|
||||||
if let Some(interval) = &health_check.interval {
|
|
||||||
args_strings.push("--health-interval".to_string());
|
|
||||||
args_strings.push(interval.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(timeout) = &health_check.timeout {
|
|
||||||
args_strings.push("--health-timeout".to_string());
|
|
||||||
args_strings.push(timeout.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(retries) = &health_check.retries {
|
|
||||||
args_strings.push("--health-retries".to_string());
|
|
||||||
args_strings.push(retries.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(start_period) = &health_check.start_period {
|
|
||||||
args_strings.push("--health-start-period".to_string());
|
|
||||||
args_strings.push(start_period.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(snapshotter_value) = &self.snapshotter {
|
|
||||||
args_strings.push("--snapshotter".to_string());
|
|
||||||
args_strings.push(snapshotter_value.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add flags to avoid BPF issues
|
|
||||||
args_strings.push("--cgroup-manager=cgroupfs".to_string());
|
|
||||||
|
|
||||||
args_strings.push(image.clone());
|
|
||||||
|
|
||||||
// Convert to string slices for the command
|
|
||||||
let args: Vec<&str> = args_strings.iter().map(|s| s.as_str()).collect();
|
|
||||||
|
|
||||||
// Execute the command
|
|
||||||
let result = execute_nerdctl_command(&args)?;
|
|
||||||
|
|
||||||
// Get the container ID from the output
|
|
||||||
let container_id = result.stdout.trim().to_string();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
name: self.name,
|
|
||||||
container_id: Some(container_id),
|
|
||||||
image: self.image,
|
|
||||||
config: self.config,
|
|
||||||
ports: self.ports,
|
|
||||||
volumes: self.volumes,
|
|
||||||
env_vars: self.env_vars,
|
|
||||||
network: self.network,
|
|
||||||
network_aliases: self.network_aliases,
|
|
||||||
cpu_limit: self.cpu_limit,
|
|
||||||
memory_limit: self.memory_limit,
|
|
||||||
memory_swap_limit: self.memory_swap_limit,
|
|
||||||
cpu_shares: self.cpu_shares,
|
|
||||||
restart_policy: self.restart_policy,
|
|
||||||
health_check: self.health_check,
|
|
||||||
detach: self.detach,
|
|
||||||
snapshotter: self.snapshotter,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,317 +0,0 @@
|
|||||||
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_operations.rs
|
|
||||||
|
|
||||||
use crate::process::CommandResult;
|
|
||||||
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
|
|
||||||
use super::container_types::{Container, ContainerStatus, ResourceUsage};
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
impl Container {
|
|
||||||
/// Start the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, NerdctlError>` - Command result or error
|
|
||||||
pub fn start(&self) -> Result<CommandResult, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_nerdctl_command(&["start", container_id])
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stop the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, NerdctlError>` - Command result or error
|
|
||||||
pub fn stop(&self) -> Result<CommandResult, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_nerdctl_command(&["stop", container_id])
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, NerdctlError>` - Command result or error
|
|
||||||
pub fn remove(&self) -> Result<CommandResult, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_nerdctl_command(&["rm", container_id])
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a command in the container
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `command` - The command to run
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, NerdctlError>` - Command result or error
|
|
||||||
pub fn exec(&self, command: &str) -> Result<CommandResult, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_nerdctl_command(&["exec", container_id, "sh", "-c", command])
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copy files between container and local filesystem
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `source` - Source path (can be container:path or local path)
|
|
||||||
/// * `dest` - Destination path (can be container:path or local path)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, NerdctlError>` - Command result or error
|
|
||||||
pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, NerdctlError> {
|
|
||||||
if self.container_id.is_some() {
|
|
||||||
execute_nerdctl_command(&["cp", source, dest])
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Export the container to a tarball
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `path` - Path to save the tarball
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, NerdctlError>` - Command result or error
|
|
||||||
pub fn export(&self, path: &str) -> Result<CommandResult, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_nerdctl_command(&["export", "-o", path, container_id])
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commit the container to an image
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `image_name` - Name for the new image
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<CommandResult, NerdctlError>` - Command result or error
|
|
||||||
pub fn commit(&self, image_name: &str) -> Result<CommandResult, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
execute_nerdctl_command(&["commit", container_id, image_name])
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get container status
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<ContainerStatus, NerdctlError>` - Container status or error
|
|
||||||
pub fn status(&self) -> Result<ContainerStatus, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
let result = execute_nerdctl_command(&["inspect", container_id])?;
|
|
||||||
|
|
||||||
// Parse the JSON output
|
|
||||||
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
|
|
||||||
Ok(json) => {
|
|
||||||
if let Some(container_json) = json.as_array().and_then(|arr| arr.first()) {
|
|
||||||
let state = container_json
|
|
||||||
.get("State")
|
|
||||||
.and_then(|state| state.get("Status"))
|
|
||||||
.and_then(|status| status.as_str())
|
|
||||||
.unwrap_or("unknown")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let status = container_json
|
|
||||||
.get("State")
|
|
||||||
.and_then(|state| state.get("Running"))
|
|
||||||
.and_then(|running| {
|
|
||||||
if running.as_bool().unwrap_or(false) {
|
|
||||||
Some("running")
|
|
||||||
} else {
|
|
||||||
Some("stopped")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or("unknown")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let created = container_json
|
|
||||||
.get("Created")
|
|
||||||
.and_then(|created| created.as_str())
|
|
||||||
.unwrap_or("unknown")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let started = container_json
|
|
||||||
.get("State")
|
|
||||||
.and_then(|state| state.get("StartedAt"))
|
|
||||||
.and_then(|started| started.as_str())
|
|
||||||
.unwrap_or("unknown")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// Get health status if available
|
|
||||||
let health_status = container_json
|
|
||||||
.get("State")
|
|
||||||
.and_then(|state| state.get("Health"))
|
|
||||||
.and_then(|health| health.get("Status"))
|
|
||||||
.and_then(|status| status.as_str())
|
|
||||||
.map(|s| s.to_string());
|
|
||||||
|
|
||||||
// Get health check output if available
|
|
||||||
let health_output = container_json
|
|
||||||
.get("State")
|
|
||||||
.and_then(|state| state.get("Health"))
|
|
||||||
.and_then(|health| health.get("Log"))
|
|
||||||
.and_then(|log| log.as_array())
|
|
||||||
.and_then(|log_array| log_array.last())
|
|
||||||
.and_then(|last_log| last_log.get("Output"))
|
|
||||||
.and_then(|output| output.as_str())
|
|
||||||
.map(|s| s.to_string());
|
|
||||||
|
|
||||||
Ok(ContainerStatus {
|
|
||||||
state,
|
|
||||||
status,
|
|
||||||
created,
|
|
||||||
started,
|
|
||||||
health_status,
|
|
||||||
health_output,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::JsonParseError("Invalid container inspect JSON".to_string()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
Err(NerdctlError::JsonParseError(format!("Failed to parse container inspect JSON: {}", e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the health status of the container
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<String, NerdctlError>` - Health status or error
|
|
||||||
pub fn health_status(&self) -> Result<String, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
let result = execute_nerdctl_command(&["inspect", "--format", "{{.State.Health.Status}}", container_id])?;
|
|
||||||
Ok(result.stdout.trim().to_string())
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get container resource usage
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Result<ResourceUsage, NerdctlError>` - Resource usage or error
|
|
||||||
pub fn resources(&self) -> Result<ResourceUsage, NerdctlError> {
|
|
||||||
if let Some(container_id) = &self.container_id {
|
|
||||||
let result = execute_nerdctl_command(&["stats", "--no-stream", container_id])?;
|
|
||||||
|
|
||||||
// Parse the output
|
|
||||||
let lines: Vec<&str> = result.stdout.lines().collect();
|
|
||||||
if lines.len() >= 2 {
|
|
||||||
let headers = lines[0];
|
|
||||||
let values = lines[1];
|
|
||||||
|
|
||||||
let headers_vec: Vec<&str> = headers.split_whitespace().collect();
|
|
||||||
let values_vec: Vec<&str> = values.split_whitespace().collect();
|
|
||||||
|
|
||||||
// Find indices for each metric
|
|
||||||
let cpu_index = headers_vec.iter().position(|&h| h.contains("CPU")).unwrap_or(0);
|
|
||||||
let mem_index = headers_vec.iter().position(|&h| h.contains("MEM")).unwrap_or(0);
|
|
||||||
let mem_perc_index = headers_vec.iter().position(|&h| h.contains("MEM%")).unwrap_or(0);
|
|
||||||
let net_in_index = headers_vec.iter().position(|&h| h.contains("NET")).unwrap_or(0);
|
|
||||||
let net_out_index = if net_in_index > 0 { net_in_index + 1 } else { 0 };
|
|
||||||
let block_in_index = headers_vec.iter().position(|&h| h.contains("BLOCK")).unwrap_or(0);
|
|
||||||
let block_out_index = if block_in_index > 0 { block_in_index + 1 } else { 0 };
|
|
||||||
let pids_index = headers_vec.iter().position(|&h| h.contains("PIDS")).unwrap_or(0);
|
|
||||||
|
|
||||||
let cpu_usage = if cpu_index < values_vec.len() {
|
|
||||||
values_vec[cpu_index].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let memory_usage = if mem_index < values_vec.len() {
|
|
||||||
values_vec[mem_index].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let memory_limit = if mem_index + 1 < values_vec.len() {
|
|
||||||
values_vec[mem_index + 1].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let memory_percentage = if mem_perc_index < values_vec.len() {
|
|
||||||
values_vec[mem_perc_index].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let network_input = if net_in_index < values_vec.len() {
|
|
||||||
values_vec[net_in_index].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let network_output = if net_out_index < values_vec.len() {
|
|
||||||
values_vec[net_out_index].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let block_input = if block_in_index < values_vec.len() {
|
|
||||||
values_vec[block_in_index].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let block_output = if block_out_index < values_vec.len() {
|
|
||||||
values_vec[block_out_index].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let pids = if pids_index < values_vec.len() {
|
|
||||||
values_vec[pids_index].to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ResourceUsage {
|
|
||||||
cpu_usage,
|
|
||||||
memory_usage,
|
|
||||||
memory_limit,
|
|
||||||
memory_percentage,
|
|
||||||
network_input,
|
|
||||||
network_output,
|
|
||||||
block_input,
|
|
||||||
block_output,
|
|
||||||
pids,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::ConversionError("Failed to parse stats output".to_string()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(NerdctlError::Other("No container ID available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_test.rs
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::super::container_types::{Container, ContainerStatus, ResourceUsage};
|
|
||||||
use super::super::NerdctlError;
|
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_container_builder_pattern() {
|
|
||||||
// Create a container with builder pattern
|
|
||||||
let container = Container::new("test-container").unwrap()
|
|
||||||
.with_port("8080:80")
|
|
||||||
.with_volume("/tmp:/data")
|
|
||||||
.with_env("TEST_ENV", "test_value")
|
|
||||||
.with_detach(true);
|
|
||||||
|
|
||||||
// Verify container properties
|
|
||||||
assert_eq!(container.name, "test-container");
|
|
||||||
assert_eq!(container.ports.len(), 1);
|
|
||||||
assert_eq!(container.ports[0], "8080:80");
|
|
||||||
assert_eq!(container.volumes.len(), 1);
|
|
||||||
assert_eq!(container.volumes[0], "/tmp:/data");
|
|
||||||
assert_eq!(container.env_vars.len(), 1);
|
|
||||||
assert_eq!(container.env_vars.get("TEST_ENV").unwrap(), "test_value");
|
|
||||||
assert_eq!(container.detach, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_container_from_image() {
|
|
||||||
// Create a container from image
|
|
||||||
let container = Container::from_image("test-container", "alpine:latest").unwrap();
|
|
||||||
|
|
||||||
// Verify container properties
|
|
||||||
assert_eq!(container.name, "test-container");
|
|
||||||
assert_eq!(container.image.as_ref().unwrap(), "alpine:latest");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_container_health_check() {
|
|
||||||
// Create a container with health check
|
|
||||||
let container = Container::new("test-container").unwrap()
|
|
||||||
.with_health_check("curl -f http://localhost/ || exit 1");
|
|
||||||
|
|
||||||
// Verify health check
|
|
||||||
assert!(container.health_check.is_some());
|
|
||||||
let health_check = container.health_check.unwrap();
|
|
||||||
assert_eq!(health_check.cmd, "curl -f http://localhost/ || exit 1");
|
|
||||||
assert!(health_check.interval.is_none());
|
|
||||||
assert!(health_check.timeout.is_none());
|
|
||||||
assert!(health_check.retries.is_none());
|
|
||||||
assert!(health_check.start_period.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_container_health_check_options() {
|
|
||||||
// Create a container with health check options
|
|
||||||
let container = Container::new("test-container").unwrap()
|
|
||||||
.with_health_check_options(
|
|
||||||
"curl -f http://localhost/ || exit 1",
|
|
||||||
Some("30s"),
|
|
||||||
Some("10s"),
|
|
||||||
Some(3),
|
|
||||||
Some("5s")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify health check options
|
|
||||||
assert!(container.health_check.is_some());
|
|
||||||
let health_check = container.health_check.unwrap();
|
|
||||||
assert_eq!(health_check.cmd, "curl -f http://localhost/ || exit 1");
|
|
||||||
assert_eq!(health_check.interval.as_ref().unwrap(), "30s");
|
|
||||||
assert_eq!(health_check.timeout.as_ref().unwrap(), "10s");
|
|
||||||
assert_eq!(health_check.retries.unwrap(), 3);
|
|
||||||
assert_eq!(health_check.start_period.as_ref().unwrap(), "5s");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/container_types.rs
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Container struct for nerdctl operations
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Container {
|
|
||||||
/// Name of the container
|
|
||||||
pub name: String,
|
|
||||||
/// Container ID
|
|
||||||
pub container_id: Option<String>,
|
|
||||||
/// Base image (if created from an image)
|
|
||||||
pub image: Option<String>,
|
|
||||||
/// Configuration options
|
|
||||||
pub config: HashMap<String, String>,
|
|
||||||
/// Port mappings
|
|
||||||
pub ports: Vec<String>,
|
|
||||||
/// Volume mounts
|
|
||||||
pub volumes: Vec<String>,
|
|
||||||
/// Environment variables
|
|
||||||
pub env_vars: HashMap<String, String>,
|
|
||||||
/// Network to connect to
|
|
||||||
pub network: Option<String>,
|
|
||||||
/// Network aliases
|
|
||||||
pub network_aliases: Vec<String>,
|
|
||||||
/// CPU limit
|
|
||||||
pub cpu_limit: Option<String>,
|
|
||||||
/// Memory limit
|
|
||||||
pub memory_limit: Option<String>,
|
|
||||||
/// Memory swap limit
|
|
||||||
pub memory_swap_limit: Option<String>,
|
|
||||||
/// CPU shares
|
|
||||||
pub cpu_shares: Option<String>,
|
|
||||||
/// Restart policy
|
|
||||||
pub restart_policy: Option<String>,
|
|
||||||
/// Health check
|
|
||||||
pub health_check: Option<HealthCheck>,
|
|
||||||
/// Whether to run in detached mode
|
|
||||||
pub detach: bool,
|
|
||||||
/// Snapshotter to use
|
|
||||||
pub snapshotter: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Health check configuration for a container
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct HealthCheck {
|
|
||||||
/// Command to run for health check
|
|
||||||
pub cmd: String,
|
|
||||||
/// Time between running the check (default: 30s)
|
|
||||||
pub interval: Option<String>,
|
|
||||||
/// Maximum time to wait for a check to complete (default: 30s)
|
|
||||||
pub timeout: Option<String>,
|
|
||||||
/// Number of consecutive failures needed to consider unhealthy (default: 3)
|
|
||||||
pub retries: Option<u32>,
|
|
||||||
/// Start period for the container to initialize before counting retries (default: 0s)
|
|
||||||
pub start_period: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Container status information
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ContainerStatus {
|
|
||||||
/// Container state (e.g., running, stopped)
|
|
||||||
pub state: String,
|
|
||||||
/// Container status
|
|
||||||
pub status: String,
|
|
||||||
/// Creation time
|
|
||||||
pub created: String,
|
|
||||||
/// Start time
|
|
||||||
pub started: String,
|
|
||||||
/// Health status (if health check is configured)
|
|
||||||
pub health_status: Option<String>,
|
|
||||||
/// Health check output (if health check is configured)
|
|
||||||
pub health_output: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Container resource usage information
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ResourceUsage {
|
|
||||||
/// CPU usage percentage
|
|
||||||
pub cpu_usage: String,
|
|
||||||
/// Memory usage
|
|
||||||
pub memory_usage: String,
|
|
||||||
/// Memory limit
|
|
||||||
pub memory_limit: String,
|
|
||||||
/// Memory usage percentage
|
|
||||||
pub memory_percentage: String,
|
|
||||||
/// Network input
|
|
||||||
pub network_input: String,
|
|
||||||
/// Network output
|
|
||||||
pub network_output: String,
|
|
||||||
/// Block input
|
|
||||||
pub block_input: String,
|
|
||||||
/// Block output
|
|
||||||
pub block_output: String,
|
|
||||||
/// PIDs
|
|
||||||
pub pids: String,
|
|
||||||
}
|
|
105
src/virt/nerdctl/containers.rs
Normal file
105
src/virt/nerdctl/containers.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/containers.rs
|
||||||
|
|
||||||
|
use crate::virt::nerdctl::execute_nerdctl_command;
|
||||||
|
use crate::process::CommandResult;
|
||||||
|
use super::NerdctlError;
|
||||||
|
|
||||||
|
/// Run a container from an image
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `image` - The image to run
|
||||||
|
/// * `name` - Optional container name
|
||||||
|
/// * `detach` - Whether to run in detached mode
|
||||||
|
/// * `ports` - Optional port mappings (e.g., ["8080:80"])
|
||||||
|
/// * `snapshotter` - Optional snapshotter to use (e.g., "native", "fuse-overlayfs")
|
||||||
|
pub fn run(image: &str, name: Option<&str>, detach: bool, ports: Option<&[&str]>, snapshotter: Option<&str>) -> Result<CommandResult, NerdctlError> {
|
||||||
|
// First, try to remove any existing container with the same name to avoid conflicts
|
||||||
|
if let Some(name_str) = name {
|
||||||
|
// Ignore errors since the container might not exist
|
||||||
|
let _ = execute_nerdctl_command(&["rm", "-f", name_str]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut args = vec!["run"];
|
||||||
|
|
||||||
|
if detach {
|
||||||
|
args.push("-d");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name_str) = name {
|
||||||
|
args.push("--name");
|
||||||
|
args.push(name_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(port_mappings) = ports {
|
||||||
|
for port in port_mappings {
|
||||||
|
args.push("-p");
|
||||||
|
args.push(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(snapshotter_value) = snapshotter {
|
||||||
|
args.push("--snapshotter");
|
||||||
|
args.push(snapshotter_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flags to avoid BPF issues
|
||||||
|
args.push("--cgroup-manager=cgroupfs");
|
||||||
|
|
||||||
|
args.push(image);
|
||||||
|
|
||||||
|
execute_nerdctl_command(&args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a command in a container
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `container` - The container ID or name
|
||||||
|
/// * [command](cci:1://file:///root/code/git.ourworld.tf/herocode/sal/src/process/run.rs:303:0-324:1) - The command to run
|
||||||
|
pub fn exec(container: &str, command: &str) -> Result<CommandResult, NerdctlError> {
|
||||||
|
execute_nerdctl_command(&["exec", container, "sh", "-c", command])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy files between container and local filesystem
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * [source](cci:1://file:///root/code/git.ourworld.tf/herocode/sal/src/process/run.rs:55:4-64:5) - Source path (can be container:path or local path)
|
||||||
|
/// * `dest` - Destination path (can be container:path or local path)
|
||||||
|
pub fn copy(source: &str, dest: &str) -> Result<CommandResult, NerdctlError> {
|
||||||
|
execute_nerdctl_command(&["cp", source, dest])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop a container
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `container` - The container ID or name
|
||||||
|
pub fn stop(container: &str) -> Result<CommandResult, NerdctlError> {
|
||||||
|
execute_nerdctl_command(&["stop", container])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a container
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `container` - The container ID or name
|
||||||
|
pub fn remove(container: &str) -> Result<CommandResult, NerdctlError> {
|
||||||
|
execute_nerdctl_command(&["rm", container])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List containers
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `all` - Whether to show all containers (including stopped ones)
|
||||||
|
pub fn list(all: bool) -> Result<CommandResult, NerdctlError> {
|
||||||
|
let mut args = vec!["ps"];
|
||||||
|
|
||||||
|
if all {
|
||||||
|
args.push("-a");
|
||||||
|
}
|
||||||
|
|
||||||
|
execute_nerdctl_command(&args)
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
// File: /root/code/git.ourworld.tf/herocode/sal/src/virt/nerdctl/health_check.rs
|
|
||||||
|
|
||||||
use super::container_types::HealthCheck;
|
|
||||||
|
|
||||||
impl HealthCheck {
|
|
||||||
/// Create a new health check with the given command
|
|
||||||
pub fn new(cmd: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
cmd: cmd.to_string(),
|
|
||||||
interval: None,
|
|
||||||
timeout: None,
|
|
||||||
retries: None,
|
|
||||||
start_period: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the interval between health checks
|
|
||||||
pub fn with_interval(mut self, interval: &str) -> Self {
|
|
||||||
self.interval = Some(interval.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the timeout for health checks
|
|
||||||
pub fn with_timeout(mut self, timeout: &str) -> Self {
|
|
||||||
self.timeout = Some(timeout.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the number of retries for health checks
|
|
||||||
pub fn with_retries(mut self, retries: u32) -> Self {
|
|
||||||
self.retries = Some(retries);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the start period for health checks
|
|
||||||
pub fn with_start_period(mut self, start_period: &str) -> Self {
|
|
||||||
self.start_period = Some(start_period.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,6 @@
|
|||||||
|
mod containers;
|
||||||
mod images;
|
mod images;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod container_types;
|
|
||||||
mod container;
|
|
||||||
mod container_builder;
|
|
||||||
mod health_check;
|
|
||||||
mod container_operations;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod container_test;
|
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
@ -48,6 +42,6 @@ impl Error for NerdctlError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub use containers::*;
|
||||||
pub use images::*;
|
pub use images::*;
|
||||||
pub use cmd::*;
|
pub use cmd::*;
|
||||||
pub use container_types::{Container, HealthCheck, ContainerStatus, ResourceUsage};
|
|
Loading…
Reference in New Issue
Block a user