This commit is contained in:
despiegk 2025-04-05 09:36:54 +02:00
parent 4c50d4b62c
commit 78db13d738
15 changed files with 1119 additions and 248 deletions

View File

@ -24,6 +24,7 @@ glob = "0.3.1" # For file pattern matching
tempfile = "3.5" # For temporary file operations tempfile = "3.5" # For temporary file operations
log = "0.4" # Logging facade log = "0.4" # Logging facade
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
rand = "0.8.5" # Random number generation
clap = "2.33" # Command-line argument parsing clap = "2.33" # Command-line argument parsing
# Optional features for specific OS functionality # Optional features for specific OS functionality

View File

@ -44,7 +44,7 @@ fn main() -> Result<(), Box<dyn Error>> {
match Container::new("existing-container") { match Container::new("existing-container") {
Ok(container) => { Ok(container) => {
if container.container_id.is_some() { if container.container_id.is_some() {
println!("Found container with ID: {}", container.container_id.unwrap()); println!("Found container with ID: {}", container.container_id.as_ref().unwrap());
// Perform operations on the existing container // Perform operations on the existing container
let status = container.status()?; let status = container.status()?;

View File

@ -0,0 +1,93 @@
use std::error::Error;
use std::fs::File;
use std::io::Write;
use tempfile::NamedTempFile;
use sal::text::TextReplacer;
fn main() -> Result<(), Box<dyn Error>> {
// Create a temporary file for our examples
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "This is a foo bar example with FOO and foo occurrences.")?;
println!("Created temporary file at: {}", temp_file.path().display());
// Example 1: Simple regex replacement
println!("\n--- Example 1: Simple regex replacement ---");
let replacer = TextReplacer::builder()
.pattern(r"\bfoo\b")
.replacement("replacement")
.regex(true)
.add_replacement()?
.build()?;
let result = replacer.replace_file(temp_file.path())?;
println!("After regex replacement: {}", result);
// Example 2: Multiple replacements in one pass
println!("\n--- Example 2: Multiple replacements in one pass ---");
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("AAA")
.add_replacement()?
.pattern("bar")
.replacement("BBB")
.add_replacement()?
.build()?;
// Write new content to the temp file
writeln!(temp_file.as_file_mut(), "foo bar foo baz")?;
temp_file.as_file_mut().flush()?;
let result = replacer.replace_file(temp_file.path())?;
println!("After multiple replacements: {}", result);
// Example 3: Case-insensitive replacement
println!("\n--- Example 3: Case-insensitive replacement ---");
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("case-insensitive")
.regex(true)
.case_insensitive(true)
.add_replacement()?
.build()?;
// Write new content to the temp file
writeln!(temp_file.as_file_mut(), "FOO foo Foo fOo")?;
temp_file.as_file_mut().flush()?;
let result = replacer.replace_file(temp_file.path())?;
println!("After case-insensitive replacement: {}", result);
// Example 4: File operations
println!("\n--- Example 4: File operations ---");
let output_file = NamedTempFile::new()?;
let replacer = TextReplacer::builder()
.pattern("example")
.replacement("EXAMPLE")
.add_replacement()?
.build()?;
// Write new content to the temp file
writeln!(temp_file.as_file_mut(), "This is an example text file.")?;
temp_file.as_file_mut().flush()?;
// Replace and write to a new file
replacer.replace_file_to(temp_file.path(), output_file.path())?;
// Read the output file to verify
let output_content = std::fs::read_to_string(output_file.path())?;
println!("Content written to new file: {}", output_content);
// Example 5: Replace in-place
println!("\n--- Example 5: Replace in-place ---");
// Replace in the same file
replacer.replace_file_in_place(temp_file.path())?;
// Read the file to verify
let updated_content = std::fs::read_to_string(temp_file.path())?;
println!("Content after in-place replacement: {}", updated_content);
Ok(())
}

View File

@ -41,6 +41,8 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
engine.register_fn("image_push", builder_image_push); engine.register_fn("image_push", builder_image_push);
engine.register_fn("image_tag", builder_image_tag); engine.register_fn("image_tag", builder_image_tag);
engine.register_fn("build", builder_build); engine.register_fn("build", builder_build);
engine.register_fn("write_content", builder_write_content);
engine.register_fn("read_content", builder_read_content);
Ok(()) Ok(())
} }
@ -146,6 +148,16 @@ pub fn builder_config(builder: &mut Builder, options: Map) -> Result<CommandResu
bah_error_to_rhai_error(builder.config(config_options)) bah_error_to_rhai_error(builder.config(config_options))
} }
/// Write content to a file in the container
pub fn builder_write_content(builder: &mut Builder, content: &str, dest_path: &str) -> Result<CommandResult, Box<EvalAltResult>> {
bah_error_to_rhai_error(builder.write_content(content, dest_path))
}
/// Read content from a file in the container
pub fn builder_read_content(builder: &mut Builder, source_path: &str) -> Result<String, Box<EvalAltResult>> {
bah_error_to_rhai_error(builder.read_content(source_path))
}
// Builder static methods // Builder static methods
pub fn builder_images(_builder: &mut Builder) -> Result<Array, Box<EvalAltResult>> { pub fn builder_images(_builder: &mut Builder) -> Result<Array, Box<EvalAltResult>> {
let images = bah_error_to_rhai_error(Builder::images())?; let images = bah_error_to_rhai_error(Builder::images())?;

View File

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

View File

@ -0,0 +1,126 @@
// buildah.rhai
// Demonstrates using buildah to create a custom image with golang and nginx,
// then using nerdctl to run a container from that image
println("Starting buildah workflow to create a custom image...");
// Define image and container names
let base_image = "ubuntu:22.04";
let container_name = "golang-nginx-container";
let final_image_name = "custom-golang-nginx:latest";
println(`Creating container '${container_name}' from base image '${base_image}'...`);
// Create a new buildah container using the builder pattern
let builder = bah_new(container_name, base_image);
// Update package lists and install golang and nginx
println("Installing packages (this may take a while)...");
// Update package lists
let update_result = builder.run("apt-get update -y");
// Install required packages
let install_result = builder.run("apt-get install -y golang nginx");
// Verify installations
let go_version = builder.run("go version");
println(`Go version: ${go_version.stdout}`);
let nginx_version = builder.run("nginx -v");
println(`Nginx version: ${nginx_version.stderr}`); // nginx outputs version to stderr
// Create a simple Go web application
println("Creating a simple Go web application...");
// Create a directory for the Go application
builder.run("mkdir -p /app");
// Create a simple Go web server
let go_app = `
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go running in a custom container!")
})
fmt.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}
`;
// Write the Go application to a file using the write_content method
builder.write_content(go_app, "/app/main.go");
// Compile the Go application
builder.run("cd /app && go build -o server main.go");
// Configure nginx to proxy to the Go application
let nginx_conf = `
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
`;
// Write the nginx configuration using the write_content method
let nginx_conf_result = builder.write_content(nginx_conf, "/etc/nginx/sites-available/default");
// Create a startup script
let startup_script = `
#!/bin/bash
# Start the Go application in the background
cd /app && ./server &
# Start nginx in the foreground
nginx -g "daemon off;"
`;
// Write the startup script using the write_content method
let startup_script_result = builder.write_content(startup_script, "/start.sh");
builder.run("chmod +x /start.sh");
// Read back the startup script to verify it was written correctly
let read_script = builder.read_content("/start.sh");
println("Startup script content verification:");
println(read_script);
// Commit the container to a new image
println(`Committing container to image '${final_image_name}'...`);
let commit_result = builder.commit(final_image_name);
// Clean up the buildah container
println("Cleaning up buildah container...");
builder.remove();
// Now use nerdctl to run a container from the new image
println("\nStarting container from the new image using nerdctl...");
// Create a container using the builder pattern
let container = nerdctl_container_from_image("golang-nginx-demo", final_image_name)
.with_detach(true)
.with_port("8080:80") // Map port 80 in the container to 8080 on the host
.with_restart_policy("unless-stopped")
.build();
// Start the container
let start_result = container.start();
println("\nWorkflow completed successfully!");
println("The web server should be running at http://localhost:8080");
println("You can check container logs with: nerdctl logs golang-nginx-demo");
println("To stop the container: nerdctl stop golang-nginx-demo");
println("To remove the container: nerdctl rm golang-nginx-demo");
"Buildah and nerdctl workflow completed successfully!"

View File

@ -1,130 +0,0 @@
// 08_nerdctl_web_server.rhai
// Demonstrates a complete workflow to set up a web server using nerdctl
// Note: This script requires nerdctl to be installed and may need root privileges
// Ensure nerdctl is installed
println("Checking if nerdctl is installed...");
// Fix the typo in nerdctl and check all required commands
let result = cmd_ensure_exists("nerdctl,runc,buildah");
println("All required commands are installed and available.");
println("Starting nerdctl web server workflow...");
// Create and use a temporary directory for all files
let work_dir = "/tmp/nerdctl";
mkdir(work_dir);
chdir(work_dir);
println(`Working in directory: ${work_dir}`);
println("\n=== Creating custom nginx configuration ===");
let config_content = `
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
`;
let config_file = `${work_dir}/custom-nginx.conf`;
// Use file_write instead of run command
file_write(config_file, config_content);
println(`Created custom nginx configuration file at ${config_file}`);
// Step 3: Create a custom index.html file
println("\n=== Creating custom index.html ===");
let html_content = `
<!DOCTYPE html>
<html>
<head>
<title>Rhai Nerdctl Demo</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
color: #333;
}
h1 {
color: #0066cc;
}
</style>
</head>
<body>
<h1>Hello from Rhai Nerdctl!</h1>
<p>This page is served by an Nginx container created using the Rhai nerdctl wrapper.</p>
<p>Current time: ${now()}</p>
</body>
</html>
`;
let html_file = `${work_dir}/index.html`;
// Use file_write instead of run command
file_write(html_file, html_content);
println(`Created custom index.html file at ${html_file}`);
println("\n=== Creating nginx container ===");
let container_name = "rhai-nginx-demo";
// First, try to remove any existing container with the same name
nerdctl_remove(container_name);
let env_map = #{}; // Create an empty map
env_map["NGINX_HOST"] = "localhost";
env_map["NGINX_PORT"] = "80";
env_map["NGINX_WORKER_PROCESSES"] = "auto";
let network_aliases = ["web-server", "nginx-demo", "http-service"];
// Create a container with a rich set of options using batch methods
let container = nerdctl_container_from_image(container_name, "nginx:latest")
.with_detach(true)
.with_ports(["8080:80"]) // Add multiple ports at once
.with_volumes([`${work_dir}:/usr/share/nginx/html`, "/var/log:/var/log/nginx"]) // Mount our work dir
.with_envs(env_map) // Add multiple environment variables at once
.with_network("bridge")
.with_network_aliases(network_aliases) // Add multiple network aliases at once
.with_cpu_limit("1.0")
.with_memory_limit("512m")
.with_memory_swap_limit("1g") // New method
.with_cpu_shares("1024") // New method
// .with_restart_policy("unless-stopped")
// .with_snapshotter("native")
// Add health check with a multiline script
// .with_health_check_options(
// `#!/bin/bash
// # Health check script for nginx container
// # This script will check if the nginx server is responding correctly
// # Try to connect to the server
// response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/ || echo "failed")
// # Check the response
// if [ "$response" = "200" ]; then
// echo "Nginx is healthy"
// exit 0
// else
// echo "Nginx is not healthy, got response: $response"
// exit 1
// fi`,
// "5s", // Interval
// "3s", // Timeout
// 3, // Retries
// "10s" // Start period
// );
// Build and start the container
println("Building and starting the container...");
let built_container = container.build();
let start_result = built_container.start();
println(`Container started: ${start_result.success}`);
println(`Successfully created and started container: ${container_name}`);
println("\nNerdctl web server workflow completed successfully!");
println("The web server is running at http://localhost:8080");
"Nerdctl web server script completed successfully!"

View File

@ -0,0 +1,102 @@
// write_read.rhai
// Demonstrates writing content to and reading content from a container
// using the write_content and read_content methods
println("Starting write/read container example...");
// Define image and container names
let base_image = "ubuntu:22.04";
let container_name = "write-read-demo";
let final_image_name = "write-read-demo:latest";
println(`Creating container '${container_name}' from base image '${base_image}'...`);
// Create a new buildah container
let builder = bah_new(container_name, base_image);
// Update package lists
println("Updating package lists...");
let update_result = builder.run("apt-get update -y");
println(`Package update result: ${update_result.success ? "Success" : "Failed"}`);
// Write a simple text file to the container
println("\nWriting content to the container...");
let text_content = "This is a test file created using write_content.\nIt supports multiple lines.\n";
let write_result = builder.write_content(text_content, "/test.txt");
println(`Write result: ${write_result.success ? "Success" : "Failed"}`);
// Write a simple HTML file to the container
println("\nWriting HTML content to the container...");
let html_content = `
<!DOCTYPE html>
<html>
<head>
<title>Write Content Demo</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
color: #333;
}
h1 {
color: #0066cc;
}
</style>
</head>
<body>
<h1>Hello from Buildah!</h1>
<p>This HTML file was created using the write_content method.</p>
</body>
</html>
`;
let html_write_result = builder.write_content(html_content, "/var/www/html/index.html");
println(`HTML write result: ${html_write_result.success ? "Success" : "Failed"}`);
// Write a simple shell script to the container
println("\nWriting shell script to the container...");
let script_content = `
#!/bin/bash
echo "This script was created using write_content"
echo "Current directory: $(pwd)"
echo "Files in current directory:"
ls -la
`;
let script_write_result = builder.write_content(script_content, "/test.sh");
println(`Script write result: ${script_write_result.success ? "Success" : "Failed"}`);
// Make the script executable
builder.run("chmod +x /test.sh");
// Read back the content we wrote
println("\nReading content from the container...");
let read_text = builder.read_content("/test.txt");
println("Text file content:");
println(read_text);
let read_html = builder.read_content("/var/www/html/index.html");
println("\nHTML file content (first 100 characters):");
println(read_html.substr(0, 100) + "...");
let read_script = builder.read_content("/test.sh");
println("\nScript file content:");
println(read_script);
// Execute the script we created
println("\nExecuting the script we created...");
let script_result = builder.run("/test.sh");
println("Script output:");
println(script_result.stdout);
// Commit the container to an image
println(`\nCommitting container to image '${final_image_name}'...`);
let commit_result = builder.commit(final_image_name);
println(`Commit result: ${commit_result.success ? "Success" : "Failed"}`);
// Clean up the buildah container
println("Cleaning up buildah container...");
builder.remove();
println("\nWrite/read example completed successfully!");
"Write/read example completed successfully!"

View File

@ -8,6 +8,7 @@ This module provides functions for text manipulation tasks such as:
- Removing indentation from multiline strings - Removing indentation from multiline strings
- Adding prefixes to multiline strings - Adding prefixes to multiline strings
- Normalizing filenames and paths - Normalizing filenames and paths
- Text replacement (regex and literal) with file operations
## Functions ## Functions
@ -68,12 +69,86 @@ assert_eq!(path_fix("./relative/path/to/DOCUMENT-123.pdf"), "./relative/path/to/
- Only normalizes the filename portion, leaving the path structure intact - Only normalizes the filename portion, leaving the path structure intact
- Handles both absolute and relative paths - Handles both absolute and relative paths
### Text Replacement
#### `TextReplacer`
A flexible text replacement utility that supports both regex and literal replacements with a builder pattern.
```rust
// Regex replacement
let replacer = TextReplacer::builder()
.pattern(r"\bfoo\b")
.replacement("bar")
.regex(true)
.add_replacement()
.unwrap()
.build()
.unwrap();
let result = replacer.replace("foo bar foo baz"); // "bar bar bar baz"
```
**Features:**
- Supports both regex and literal string replacements
- Builder pattern for fluent configuration
- Multiple replacements in a single pass
- Case-insensitive matching (for regex replacements)
- File reading and writing operations
#### Multiple Replacements
```rust
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("qux")
.add_replacement()
.unwrap()
.pattern("bar")
.replacement("baz")
.add_replacement()
.unwrap()
.build()
.unwrap();
let result = replacer.replace("foo bar foo"); // "qux baz qux"
```
#### File Operations
```rust
// Replace in a file and get the result as a string
let result = replacer.replace_file("input.txt")?;
// Replace in a file and write back to the same file
replacer.replace_file_in_place("input.txt")?;
// Replace in a file and write to a new file
replacer.replace_file_to("input.txt", "output.txt")?;
```
#### Case-Insensitive Matching
```rust
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("bar")
.regex(true)
.case_insensitive(true)
.add_replacement()
.unwrap()
.build()
.unwrap();
let result = replacer.replace("FOO foo Foo"); // "bar bar bar"
```
## Usage ## Usage
Import the functions from the module: Import the functions from the module:
```rust ```rust
use your_crate::text::{dedent, prefix, name_fix, path_fix}; use your_crate::text::{dedent, prefix, name_fix, path_fix, TextReplacer};
``` ```
## Examples ## Examples

View File

@ -10,7 +10,8 @@ pub fn name_fix(text: &str) -> String {
// Replace specific characters with underscore // Replace specific characters with underscore
if c.is_whitespace() || c == ',' || c == '-' || c == '"' || c == '\'' || if c.is_whitespace() || c == ',' || c == '-' || c == '"' || c == '\'' ||
c == '#' || c == '!' || c == '(' || c == ')' || c == '[' || c == ']' || c == '#' || c == '!' || c == '(' || c == ')' || c == '[' || c == ']' ||
c == '=' || c == '+' || c == '<' || c == '>' { c == '=' || c == '+' || c == '<' || c == '>' || c == '@' || c == '$' ||
c == '%' || c == '^' || c == '&' || c == '*' {
// Only add underscore if the last character wasn't an underscore // Only add underscore if the last character wasn't an underscore
if !last_was_underscore { if !last_was_underscore {
result.push('_'); result.push('_');

View File

@ -1,5 +1,7 @@
mod dedent; mod dedent;
mod fix; mod fix;
mod replace;
pub use dedent::*; pub use dedent::*;
pub use fix::*; pub use fix::*;
pub use replace::*;

293
src/text/replace.rs Normal file
View File

@ -0,0 +1,293 @@
use regex::Regex;
use std::fs;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
/// Represents the type of replacement to perform.
#[derive(Clone)]
pub enum ReplaceMode {
/// Regex-based replacement using the `regex` crate
Regex(Regex),
/// Literal substring replacement (non-regex)
Literal(String),
}
/// A single replacement operation with a pattern and replacement text
#[derive(Clone)]
pub struct ReplacementOperation {
mode: ReplaceMode,
replacement: String,
}
impl ReplacementOperation {
/// Applies this replacement operation to the input text
fn apply(&self, input: &str) -> String {
match &self.mode {
ReplaceMode::Regex(re) => re.replace_all(input, self.replacement.as_str()).to_string(),
ReplaceMode::Literal(search) => input.replace(search, &self.replacement),
}
}
}
/// Text replacer that can perform multiple replacement operations
/// in a single pass over the input text.
pub struct TextReplacer {
operations: Vec<ReplacementOperation>,
}
impl TextReplacer {
/// Creates a new builder for configuring a TextReplacer
pub fn builder() -> TextReplacerBuilder {
TextReplacerBuilder::default()
}
/// Applies all configured replacement operations to the input text
pub fn replace(&self, input: &str) -> String {
let mut result = input.to_string();
// Apply each replacement operation in sequence
for op in &self.operations {
result = op.apply(&result);
}
result
}
/// Reads a file, applies all replacements, and returns the result as a string
pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
let mut file = fs::File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(self.replace(&content))
}
/// Reads a file, applies all replacements, and writes the result back to the file
pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let content = self.replace_file(&path)?;
fs::write(path, content)?;
Ok(())
}
/// Reads a file, applies all replacements, and writes the result to a new file
pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
&self,
input_path: P1,
output_path: P2
) -> io::Result<()> {
let content = self.replace_file(&input_path)?;
fs::write(output_path, content)?;
Ok(())
}
}
/// Builder for the TextReplacer.
#[derive(Default)]
pub struct TextReplacerBuilder {
operations: Vec<ReplacementOperation>,
pattern: Option<String>,
replacement: Option<String>,
use_regex: bool,
case_insensitive: bool,
}
impl TextReplacerBuilder {
/// Sets the pattern to search for
pub fn pattern(mut self, pat: &str) -> Self {
self.pattern = Some(pat.to_string());
self
}
/// Sets the replacement text
pub fn replacement(mut self, rep: &str) -> Self {
self.replacement = Some(rep.to_string());
self
}
/// Sets whether to use regex
pub fn regex(mut self, yes: bool) -> Self {
self.use_regex = yes;
self
}
/// Sets whether the replacement should be case-insensitive
pub fn case_insensitive(mut self, yes: bool) -> Self {
self.case_insensitive = yes;
self
}
/// Adds another replacement operation to the chain and resets the builder for a new operation
pub fn and(mut self) -> Self {
self.add_current_operation();
self
}
// Helper method to add the current operation to the list
fn add_current_operation(&mut self) -> bool {
if let Some(pattern) = self.pattern.take() {
let replacement = self.replacement.take().unwrap_or_default();
let use_regex = self.use_regex;
let case_insensitive = self.case_insensitive;
// Reset current settings
self.use_regex = false;
self.case_insensitive = false;
// Create the replacement mode
let mode = if use_regex {
let mut regex_pattern = pattern;
// If case insensitive, add the flag to the regex pattern
if case_insensitive && !regex_pattern.starts_with("(?i)") {
regex_pattern = format!("(?i){}", regex_pattern);
}
match Regex::new(&regex_pattern) {
Ok(re) => ReplaceMode::Regex(re),
Err(_) => return false, // Failed to compile regex
}
} else {
// For literal replacement, we'll handle case insensitivity differently
// since String::replace doesn't have a case-insensitive option
if case_insensitive {
return false; // Case insensitive not supported for literal
}
ReplaceMode::Literal(pattern)
};
self.operations.push(ReplacementOperation {
mode,
replacement,
});
true
} else {
false
}
}
/// Builds the TextReplacer with all configured replacement operations
pub fn build(mut self) -> Result<TextReplacer, String> {
// If there's a pending replacement operation, add it
self.add_current_operation();
// Ensure we have at least one replacement operation
if self.operations.is_empty() {
return Err("No replacement operations configured".to_string());
}
Ok(TextReplacer {
operations: self.operations,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_regex_replace() {
let replacer = TextReplacer::builder()
.pattern(r"\bfoo\b")
.replacement("bar")
.regex(true)
.build()
.unwrap();
let input = "foo bar foo baz";
let output = replacer.replace(input);
assert_eq!(output, "bar bar bar baz");
}
#[test]
fn test_literal_replace() {
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("qux")
.regex(false)
.build()
.unwrap();
let input = "foo bar foo baz";
let output = replacer.replace(input);
assert_eq!(output, "qux bar qux baz");
}
#[test]
fn test_multiple_replacements() {
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("qux")
.and()
.pattern("bar")
.replacement("baz")
.build()
.unwrap();
let input = "foo bar foo";
let output = replacer.replace(input);
assert_eq!(output, "qux baz qux");
}
#[test]
fn test_case_insensitive_regex() {
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("bar")
.regex(true)
.case_insensitive(true)
.build()
.unwrap();
let input = "FOO foo Foo";
let output = replacer.replace(input);
assert_eq!(output, "bar bar bar");
}
#[test]
fn test_file_operations() -> io::Result<()> {
// Create a temporary file
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "foo bar foo baz")?;
// Flush the file to ensure content is written
temp_file.as_file_mut().flush()?;
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("qux")
.build()
.unwrap();
// Test replace_file
let result = replacer.replace_file(temp_file.path())?;
assert_eq!(result, "qux bar qux baz\n");
// Test replace_file_in_place
replacer.replace_file_in_place(temp_file.path())?;
// Verify the file was updated - need to seek to beginning of file first
let mut content = String::new();
temp_file.as_file_mut().seek(SeekFrom::Start(0))?;
temp_file.as_file_mut().read_to_string(&mut content)?;
assert_eq!(content, "qux bar qux baz\n");
// Test replace_file_to with a new temporary file
let output_file = NamedTempFile::new()?;
replacer.replace_file_to(temp_file.path(), output_file.path())?;
// Verify the output file has the replaced content
let mut output_content = String::new();
fs::File::open(output_file.path())?.read_to_string(&mut output_content)?;
assert_eq!(output_content, "qux bar qux baz\n");
Ok(())
}
}

298
src/text/template.rs Normal file
View File

@ -0,0 +1,298 @@
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::Path;
use tera::{Context, Tera};
/// A builder for creating and rendering templates using the Tera template engine.
pub struct TemplateBuilder {
template_path: String,
context: Context,
tera: Option<Tera>,
}
impl TemplateBuilder {
/// Creates a new TemplateBuilder with the specified template path.
///
/// # Arguments
///
/// * `template_path` - The path to the template file
///
/// # Returns
///
/// A new TemplateBuilder instance
///
/// # Example
///
/// ```
/// use sal::text::TemplateBuilder;
///
/// let builder = TemplateBuilder::open("templates/example.html");
/// ```
pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> {
let path_str = template_path.as_ref().to_string_lossy().to_string();
// Verify the template file exists
if !Path::new(&path_str).exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Template file not found: {}", path_str),
));
}
Ok(Self {
template_path: path_str,
context: Context::new(),
tera: None,
})
}
/// Adds a variable to the template context.
///
/// # Arguments
///
/// * `name` - The name of the variable to add
/// * `value` - The value to associate with the variable
///
/// # Returns
///
/// The builder instance for method chaining
///
/// # Example
///
/// ```
/// use sal::text::TemplateBuilder;
///
/// let builder = TemplateBuilder::open("templates/example.html")?
/// .add_var("title", "Hello World")
/// .add_var("username", "John Doe");
/// ```
pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
where
S: AsRef<str>,
V: serde::Serialize,
{
self.context.insert(name.as_ref(), &value);
self
}
/// Adds multiple variables to the template context from a HashMap.
///
/// # Arguments
///
/// * `vars` - A HashMap containing variable names and values
///
/// # Returns
///
/// The builder instance for method chaining
///
/// # Example
///
/// ```
/// use sal::text::TemplateBuilder;
/// use std::collections::HashMap;
///
/// let mut vars = HashMap::new();
/// vars.insert("title", "Hello World");
/// vars.insert("username", "John Doe");
///
/// let builder = TemplateBuilder::open("templates/example.html")?
/// .add_vars(vars);
/// ```
pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self
where
S: AsRef<str>,
V: serde::Serialize,
{
for (name, value) in vars {
self.context.insert(name.as_ref(), &value);
}
self
}
/// Initializes the Tera template engine with the template file.
///
/// This method is called automatically by render() if not called explicitly.
///
/// # Returns
///
/// The builder instance for method chaining
fn initialize_tera(&mut self) -> Result<(), tera::Error> {
if self.tera.is_none() {
// Create a new Tera instance with just this template
let mut tera = Tera::default();
// Read the template content
let template_content = fs::read_to_string(&self.template_path)
.map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?;
// Add the template to Tera
let template_name = Path::new(&self.template_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("template");
tera.add_raw_template(template_name, &template_content)?;
self.tera = Some(tera);
}
Ok(())
}
/// Renders the template with the current context.
///
/// # Returns
///
/// The rendered template as a string
///
/// # Example
///
/// ```
/// use sal::text::TemplateBuilder;
///
/// let result = TemplateBuilder::open("templates/example.html")?
/// .add_var("title", "Hello World")
/// .add_var("username", "John Doe")
/// .render()?;
///
/// println!("Rendered template: {}", result);
/// ```
pub fn render(&mut self) -> Result<String, tera::Error> {
// Initialize Tera if not already done
self.initialize_tera()?;
// Get the template name
let template_name = Path::new(&self.template_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("template");
// Render the template
let tera = self.tera.as_ref().unwrap();
tera.render(template_name, &self.context)
}
/// Renders the template and writes the result to a file.
///
/// # Arguments
///
/// * `output_path` - The path where the rendered template should be written
///
/// # Returns
///
/// Result indicating success or failure
///
/// # Example
///
/// ```
/// use sal::text::TemplateBuilder;
///
/// TemplateBuilder::open("templates/example.html")?
/// .add_var("title", "Hello World")
/// .add_var("username", "John Doe")
/// .render_to_file("output.html")?;
/// ```
pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> {
let rendered = self.render().map_err(|e| {
io::Error::new(io::ErrorKind::Other, format!("Template rendering error: {}", e))
})?;
fs::write(output_path, rendered)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "Hello, {{ name }}! Welcome to {{ place }}.")?;
temp_file.flush()?;
// Create a template builder and add variables
let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder
.add_var("name", "John")
.add_var("place", "Rust");
// Render the template
let result = builder.render()?;
assert_eq!(result, "Hello, John! Welcome to Rust.\n");
Ok(())
}
#[test]
fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "{% if show_greeting %}Hello, {{ name }}!{% endif %}")?;
writeln!(temp_file, "{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}")?;
temp_file.flush()?;
// Create a template builder and add variables
let mut builder = TemplateBuilder::open(temp_file.path())?;
// Add variables including a boolean and a vector
builder = builder
.add_var("name", "Alice")
.add_var("show_greeting", true)
.add_var("items", vec!["apple", "banana", "cherry"]);
// Render the template
let result = builder.render()?;
assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n");
Ok(())
}
#[test]
fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "{{ greeting }}, {{ name }}!")?;
temp_file.flush()?;
// Create a HashMap of variables
let mut vars = HashMap::new();
vars.insert("greeting", "Hi");
vars.insert("name", "Bob");
// Create a template builder and add variables from HashMap
let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder.add_vars(vars);
// Render the template
let result = builder.render()?;
assert_eq!(result, "Hi, Bob!\n");
Ok(())
}
#[test]
fn test_render_to_file() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "{{ message }}")?;
temp_file.flush()?;
// Create an output file
let output_file = NamedTempFile::new()?;
// Create a template builder, add a variable, and render to file
let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder.add_var("message", "This is a test");
builder.render_to_file(output_file.path())?;
// Read the output file and verify its contents
let content = fs::read_to_string(output_file.path())?;
assert_eq!(content, "This is a test\n");
Ok(())
}
}

View File

@ -1,11 +1,26 @@
// Basic buildah operations for container management // Basic buildah operations for container management
use std::process::Command; use std::process::Command;
use crate::process::CommandResult; use crate::process::CommandResult;
use super::BuildahError; use super::{BuildahError, Builder};
/// Execute a buildah command and return the result /// Execute a buildah command and return the result
///
/// # Arguments
///
/// * `args` - The command arguments
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError> { pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError> {
// Get the current thread-local Builder instance if available
let debug = thread_local_debug();
if debug {
println!("Executing buildah command: buildah {}", args.join(" "));
}
let output = Command::new("buildah") let output = Command::new("buildah")
.args(args) .args(args)
.output(); .output();
@ -22,15 +37,93 @@ pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahEr
code: output.status.code().unwrap_or(-1), code: output.status.code().unwrap_or(-1),
}; };
if debug {
if !result.stdout.is_empty() {
println!("Command stdout: {}", result.stdout);
}
if !result.stderr.is_empty() {
println!("Command stderr: {}", result.stderr);
}
}
if result.success { if result.success {
if debug {
println!("Command succeeded with code {}", result.code);
}
Ok(result) Ok(result)
} else { } else {
Err(BuildahError::CommandFailed(format!("Command failed with code {}: {}", let error_msg = format!("Command failed with code {}: {}",
result.code, result.stderr.trim()))) result.code, result.stderr.trim());
if debug {
println!("Command failed: {}", error_msg);
}
Err(BuildahError::CommandFailed(error_msg))
} }
}, },
Err(e) => { Err(e) => {
if debug {
println!("Command execution failed: {}", e);
}
Err(BuildahError::CommandExecutionFailed(e)) Err(BuildahError::CommandExecutionFailed(e))
} }
} }
} }
// Thread-local storage for debug flag
thread_local! {
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
}
/// Set the debug flag for the current thread
pub fn set_thread_local_debug(debug: bool) {
DEBUG.with(|cell| {
*cell.borrow_mut() = debug;
});
}
/// Get the debug flag for the current thread
pub fn thread_local_debug() -> bool {
DEBUG.with(|cell| {
*cell.borrow()
})
}
/// Execute a buildah command with debug output
///
/// # Arguments
///
/// * `args` - The command arguments
/// * `builder` - Reference to a Builder instance for debug output
///
/// # Returns
///
/// * `Result<CommandResult, BuildahError>` - Command result or error
pub fn execute_buildah_command_with_debug(args: &[&str], builder: &Builder) -> Result<CommandResult, BuildahError> {
if builder.debug() {
println!("Executing buildah command: buildah {}", args.join(" "));
}
let result = execute_buildah_command(args);
if builder.debug() {
match &result {
Ok(cmd_result) => {
if !cmd_result.stdout.is_empty() {
println!("Command stdout: {}", cmd_result.stdout);
}
if !cmd_result.stderr.is_empty() {
println!("Command stderr: {}", cmd_result.stderr);
}
println!("Command succeeded with code {}", cmd_result.code);
},
Err(e) => {
println!("Command failed: {}", e);
}
}
}
result
}

View File

@ -7,6 +7,7 @@ use super::health_check_script::prepare_health_check_command;
impl Container { impl Container {
/// Reset the container configuration to defaults while keeping the name and image /// Reset the container configuration to defaults while keeping the name and image
/// If the container exists, it will be stopped and removed.
/// ///
/// # Returns /// # Returns
/// ///
@ -14,12 +15,22 @@ impl Container {
pub fn reset(mut self) -> Self { pub fn reset(mut self) -> Self {
let name = self.name; let name = self.name;
let image = self.image.clone(); let image = self.image.clone();
let container_id = self.container_id.clone();
// Create a new container with just the name, image, and container_id // If container exists, stop and remove it
if let Some(container_id) = &self.container_id {
println!("Container exists. Stopping and removing container '{}'...", name);
// Try to stop the container
let _ = execute_nerdctl_command(&["stop", container_id]);
// Try to remove the container
let _ = execute_nerdctl_command(&["rm", container_id]);
}
// Create a new container with just the name and image, but no container_id
Self { Self {
name, name,
container_id, container_id: None, // Reset container_id to None since we removed the container
image, image,
config: std::collections::HashMap::new(), config: std::collections::HashMap::new(),
ports: Vec::new(), ports: Vec::new(),