...
This commit is contained in:
parent
4c50d4b62c
commit
78db13d738
@ -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
|
||||||
|
@ -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()?;
|
||||||
|
93
src/examples/text_replace_example.rs
Normal file
93
src/examples/text_replace_example.rs
Normal 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(())
|
||||||
|
}
|
@ -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())?;
|
||||||
|
@ -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!"
|
|
126
src/rhaiexamples/buildah.rhai
Normal file
126
src/rhaiexamples/buildah.rhai
Normal 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!"
|
@ -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!"
|
|
102
src/rhaiexamples/write_read.rhai
Normal file
102
src/rhaiexamples/write_read.rhai
Normal 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!"
|
@ -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
|
||||||
|
@ -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('_');
|
||||||
|
@ -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
293
src/text/replace.rs
Normal 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(®ex_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
298
src/text/template.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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(),
|
||||||
|
Loading…
Reference in New Issue
Block a user