sal/buildah_builder_implementation_plan.md
2025-04-05 04:45:56 +02:00

28 KiB

Buildah Builder Implementation Plan

Introduction

This document outlines the plan for changing the buildah interface in the @src/virt/buildah module to use a builder object pattern. The current implementation uses standalone functions, which makes the interface less clear and harder to use. The new implementation will use a builder object with methods, which will make the interface more intuitive and easier to use.

Current Architecture

The current buildah implementation has:

  • Standalone functions in the buildah module (from, run, images, etc.)
  • Functions that operate on container IDs passed as parameters
  • No state maintained between function calls
  • Rhai wrappers that expose these functions to Rhai scripts

Example of current usage:

// Create a container
let result = buildah::from("fedora:latest")?;
let container_id = result.stdout.trim();

// Run a command in the container
buildah::run(container_id, "dnf install -y nginx")?;

// Copy a file into the container
buildah::bah_copy(container_id, "./example.conf", "/etc/example.conf")?;

// Commit the container to create a new image
buildah::bah_commit(container_id, "my-nginx:latest")?;

Proposed Architecture

We'll change to a builder object pattern where:

  • A Builder struct is created with a new() method that takes a name and image
  • All operations (including those not specific to a container) are methods on the Builder
  • The Builder maintains state (like container ID) between method calls
  • Methods return operation results (CommandResult or other types) for error handling
  • Rhai wrappers expose the Builder and its methods to Rhai scripts

Example of proposed usage:

// Create a builder
let builder = Builder::new("my-container", "fedora:latest")?;

// Run a command in the container
builder.run("dnf install -y nginx")?;

// Copy a file into the container
builder.copy("./example.conf", "/etc/example.conf")?;

// Commit the container to create a new image
builder.commit("my-nginx:latest")?;

Class Diagram

classDiagram
    class Builder {
        -String name
        -String container_id
        -String image
        +new(name: String, image: String) -> Result<Builder, BuildahError>
        +run(command: String) -> Result<CommandResult, BuildahError>
        +run_with_isolation(command: String, isolation: String) -> Result<CommandResult, BuildahError>
        +add(source: String, dest: String) -> Result<CommandResult, BuildahError>
        +copy(source: String, dest: String) -> Result<CommandResult, BuildahError>
        +commit(image_name: String) -> Result<CommandResult, BuildahError>
        +remove() -> Result<CommandResult, BuildahError>
        +config(options: HashMap<String, String>) -> Result<CommandResult, BuildahError>
    }
    
    class BuilderStatic {
        +images() -> Result<Vec<Image>, BuildahError>
        +image_remove(image: String) -> Result<CommandResult, BuildahError>
        +image_pull(image: String, tls_verify: bool) -> Result<CommandResult, BuildahError>
        +image_push(image: String, destination: String, tls_verify: bool) -> Result<CommandResult, BuildahError>
        +image_tag(image: String, new_name: String) -> Result<CommandResult, BuildahError>
        +image_commit(container: String, image_name: String, format: Option<String>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError>
        +build(tag: Option<String>, context_dir: String, file: String, isolation: Option<String>) -> Result<CommandResult, BuildahError>
    }
    
    Builder --|> BuilderStatic : Static methods

Implementation Plan

Step 1: Create the Builder Struct

  1. Create a new file src/virt/buildah/builder.rs
  2. Define the Builder struct with fields for name, container_id, and image
  3. Implement the new() method that creates a container from the image and returns a Builder instance
  4. Implement methods for all container operations (run, add, copy, commit, etc.)
  5. Implement methods for all image operations (images, image_remove, image_pull, etc.)

Step 2: Update the Module Structure

  1. Update src/virt/buildah/mod.rs to include the new builder module
  2. Re-export the Builder struct and its methods
  3. Keep the existing functions for backward compatibility (marked as deprecated)

Step 3: Update the Rhai Wrapper

  1. Update src/rhai/buildah.rs to register the Builder type with the Rhai engine
  2. Register all Builder methods with the Rhai engine
  3. Create Rhai-friendly constructor for the Builder
  4. Update the existing function wrappers to use the new Builder (or keep them for backward compatibility)

Step 4: Update Examples and Tests

  1. Update examples/buildah.rs to use the new Builder pattern
  2. Update rhaiexamples/04_buildah_operations.rhai to use the new Builder pattern
  3. Update any tests to use the new Builder pattern

Detailed Implementation

1. Builder Struct Definition

// src/virt/buildah/builder.rs
pub struct Builder {
    name: String,
    container_id: Option<String>,
    image: String,
}

impl Builder {
    pub fn new(name: &str, image: &str) -> Result<Self, BuildahError> {
        let result = execute_buildah_command(&["from", "--name", name, image])?;
        let container_id = result.stdout.trim().to_string();
        
        Ok(Self {
            name: name.to_string(),
            container_id: Some(container_id),
            image: image.to_string(),
        })
    }
    
    // Container methods
    pub fn run(&self, command: &str) -> Result<CommandResult, BuildahError> {
        if let Some(container_id) = &self.container_id {
            execute_buildah_command(&["run", container_id, "sh", "-c", command])
        } else {
            Err(BuildahError::Other("No container ID available".to_string()))
        }
    }
    
    pub fn run_with_isolation(&self, command: &str, isolation: &str) -> Result<CommandResult, BuildahError> {
        if let Some(container_id) = &self.container_id {
            execute_buildah_command(&["run", "--isolation", isolation, container_id, "sh", "-c", command])
        } else {
            Err(BuildahError::Other("No container ID available".to_string()))
        }
    }
    
    pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
        if let Some(container_id) = &self.container_id {
            execute_buildah_command(&["copy", container_id, source, dest])
        } else {
            Err(BuildahError::Other("No container ID available".to_string()))
        }
    }
    
    pub fn add(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
        if let Some(container_id) = &self.container_id {
            execute_buildah_command(&["add", container_id, source, dest])
        } else {
            Err(BuildahError::Other("No container ID available".to_string()))
        }
    }
    
    pub fn commit(&self, image_name: &str) -> Result<CommandResult, BuildahError> {
        if let Some(container_id) = &self.container_id {
            execute_buildah_command(&["commit", container_id, image_name])
        } else {
            Err(BuildahError::Other("No container ID available".to_string()))
        }
    }
    
    pub fn remove(&self) -> Result<CommandResult, BuildahError> {
        if let Some(container_id) = &self.container_id {
            execute_buildah_command(&["rm", container_id])
        } else {
            Err(BuildahError::Other("No container ID available".to_string()))
        }
    }
    
    pub fn config(&self, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> {
        if let Some(container_id) = &self.container_id {
            let mut args_owned: Vec<String> = Vec::new();
            args_owned.push("config".to_string());
            
            // Process options map
            for (key, value) in options.iter() {
                let option_name = format!("--{}", key);
                args_owned.push(option_name);
                args_owned.push(value.clone());
            }
            
            args_owned.push(container_id.clone());
            
            // Convert Vec<String> to Vec<&str> for execute_buildah_command
            let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
            
            execute_buildah_command(&args)
        } else {
            Err(BuildahError::Other("No container ID available".to_string()))
        }
    }
    
    // Static methods
    pub fn images() -> Result<Vec<Image>, BuildahError> {
        // Implementation from current images() function
        let result = execute_buildah_command(&["images", "--json"])?;
        
        // Try to parse the JSON output
        match serde_json::from_str::<serde_json::Value>(&result.stdout) {
            Ok(json) => {
                if let serde_json::Value::Array(images_json) = json {
                    let mut images = Vec::new();
                    
                    for image_json in images_json {
                        // Extract image ID
                        let id = match image_json.get("id").and_then(|v| v.as_str()) {
                            Some(id) => id.to_string(),
                            None => return Err(BuildahError::ConversionError("Missing image ID".to_string())),
                        };
                        
                        // Extract image names
                        let names = match image_json.get("names").and_then(|v| v.as_array()) {
                            Some(names_array) => {
                                let mut names_vec = Vec::new();
                                for name_value in names_array {
                                    if let Some(name_str) = name_value.as_str() {
                                        names_vec.push(name_str.to_string());
                                    }
                                }
                                names_vec
                            },
                            None => Vec::new(), // Empty vector if no names found
                        };
                        
                        // Extract image size
                        let size = match image_json.get("size").and_then(|v| v.as_str()) {
                            Some(size) => size.to_string(),
                            None => "Unknown".to_string(), // Default value if size not found
                        };
                        
                        // Extract creation timestamp
                        let created = match image_json.get("created").and_then(|v| v.as_str()) {
                            Some(created) => created.to_string(),
                            None => "Unknown".to_string(), // Default value if created not found
                        };
                        
                        // Create Image struct and add to vector
                        images.push(Image {
                            id,
                            names,
                            size,
                            created,
                        });
                    }
                    
                    Ok(images)
                } else {
                    Err(BuildahError::JsonParseError("Expected JSON array".to_string()))
                }
            },
            Err(e) => {
                Err(BuildahError::JsonParseError(format!("Failed to parse image list JSON: {}", e)))
            }
        }
    }
    
    pub fn image_remove(image: &str) -> Result<CommandResult, BuildahError> {
        execute_buildah_command(&["rmi", image])
    }
    
    pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
        let mut args = vec!["pull"];
        
        if !tls_verify {
            args.push("--tls-verify=false");
        }
        
        args.push(image);
        
        execute_buildah_command(&args)
    }
    
    pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
        let mut args = vec!["push"];
        
        if !tls_verify {
            args.push("--tls-verify=false");
        }
        
        args.push(image);
        args.push(destination);
        
        execute_buildah_command(&args)
    }
    
    pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, BuildahError> {
        execute_buildah_command(&["tag", image, new_name])
    }
    
    pub fn image_commit(container: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError> {
        let mut args = vec!["commit"];
        
        if let Some(format_str) = format {
            args.push("--format");
            args.push(format_str);
        }
        
        if squash {
            args.push("--squash");
        }
        
        if rm {
            args.push("--rm");
        }
        
        args.push(container);
        args.push(image_name);
        
        execute_buildah_command(&args)
    }
    
    pub fn build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError> {
        let mut args = Vec::new();
        args.push("build");
        
        if let Some(tag_value) = tag {
            args.push("-t");
            args.push(tag_value);
        }
        
        if let Some(isolation_value) = isolation {
            args.push("--isolation");
            args.push(isolation_value);
        }
        
        args.push("-f");
        args.push(file);
        
        args.push(context_dir);
        
        execute_buildah_command(&args)
    }
}

2. Updated Module Structure

// src/virt/buildah/mod.rs
mod containers;
mod images;
mod cmd;
mod builder;
#[cfg(test)]
mod containers_test;

use std::fmt;
use std::error::Error;
use std::io;

/// Error type for buildah operations
#[derive(Debug)]
pub enum BuildahError {
    /// The buildah command failed to execute
    CommandExecutionFailed(io::Error),
    /// The buildah command executed but returned an error
    CommandFailed(String),
    /// Failed to parse JSON output
    JsonParseError(String),
    /// Failed to convert data
    ConversionError(String),
    /// Generic error
    Other(String),
}

impl fmt::Display for BuildahError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BuildahError::CommandExecutionFailed(e) => write!(f, "Failed to execute buildah command: {}", e),
            BuildahError::CommandFailed(e) => write!(f, "Buildah command failed: {}", e),
            BuildahError::JsonParseError(e) => write!(f, "Failed to parse JSON: {}", e),
            BuildahError::ConversionError(e) => write!(f, "Conversion error: {}", e),
            BuildahError::Other(e) => write!(f, "{}", e),
        }
    }
}

impl Error for BuildahError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            BuildahError::CommandExecutionFailed(e) => Some(e),
            _ => None,
        }
    }
}

// Re-export the Builder
pub use builder::Builder;

// Re-export existing functions for backward compatibility
#[deprecated(since = "0.2.0", note = "Use Builder::new() instead")]
pub use containers::*;
#[deprecated(since = "0.2.0", note = "Use Builder methods instead")]
pub use images::*;
pub use cmd::*;

3. Rhai Wrapper Changes

// src/rhai/buildah.rs
//! Rhai wrappers for Buildah module functions
//!
//! This module provides Rhai wrappers for the functions in the Buildah module.

use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
use std::collections::HashMap;
use crate::virt::buildah::{self, BuildahError, Image, Builder};
use crate::process::CommandResult;

/// Register Buildah module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
    // Register types
    register_bah_types(engine)?;
    
    // Register Builder constructor
    engine.register_fn("bah_new", bah_new);
    
    // Register Builder instance methods
    engine.register_fn("run", |builder: &mut Builder, command: &str| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(builder.run(command))
    });
    
    engine.register_fn("run_with_isolation", |builder: &mut Builder, command: &str, isolation: &str| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(builder.run_with_isolation(command, isolation))
    });
    
    engine.register_fn("copy", |builder: &mut Builder, source: &str, dest: &str| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(builder.copy(source, dest))
    });
    
    engine.register_fn("add", |builder: &mut Builder, source: &str, dest: &str| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(builder.add(source, dest))
    });
    
    engine.register_fn("commit", |builder: &mut Builder, image_name: &str| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(builder.commit(image_name))
    });
    
    engine.register_fn("remove", |builder: &mut Builder| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(builder.remove())
    });
    
    engine.register_fn("config", |builder: &mut Builder, options: Map| -> Result<CommandResult, Box<EvalAltResult>> {
        // Convert Rhai Map to Rust HashMap
        let config_options = convert_map_to_hashmap(options)?;
        bah_error_to_rhai_error(builder.config(config_options))
    });
    
    // Register Builder static methods
    engine.register_fn("images", |_: &mut Builder| -> Result<Array, Box<EvalAltResult>> {
        let images = bah_error_to_rhai_error(Builder::images())?;
        
        // Convert Vec<Image> to Rhai Array
        let mut array = Array::new();
        for image in images {
            array.push(Dynamic::from(image));
        }
        
        Ok(array)
    });
    
    engine.register_fn("image_remove", |_: &mut Builder, image: &str| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(Builder::image_remove(image))
    });
    
    engine.register_fn("image_pull", |_: &mut Builder, image: &str, tls_verify: bool| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(Builder::image_pull(image, tls_verify))
    });
    
    engine.register_fn("image_push", |_: &mut Builder, image: &str, destination: &str, tls_verify: bool| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify))
    });
    
    engine.register_fn("image_tag", |_: &mut Builder, image: &str, new_name: &str| -> Result<CommandResult, Box<EvalAltResult>> {
        bah_error_to_rhai_error(Builder::image_tag(image, new_name))
    });
    
    // Register legacy functions for backward compatibility
    register_legacy_functions(engine)?;
    
    Ok(())
}

/// Register Buildah module types with the Rhai engine
fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
    // Register Builder type
    engine.register_type_with_name::<Builder>("BuildahBuilder");
    
    // Register getters for Builder properties
    engine.register_get("container_id", |builder: &mut Builder| {
        match builder.container_id() {
            Some(id) => id.clone(),
            None => "".to_string(),
        }
    });
    
    engine.register_get("name", |builder: &mut Builder| builder.name().to_string());
    engine.register_get("image", |builder: &mut Builder| builder.image().to_string());
    
    // Register Image type and methods (same as before)
    engine.register_type_with_name::<Image>("BuildahImage");
    
    // Register getters for Image properties
    engine.register_get("id", |img: &mut Image| img.id.clone());
    engine.register_get("names", |img: &mut Image| {
        let mut array = Array::new();
        for name in &img.names {
            array.push(Dynamic::from(name.clone()));
        }
        array
    });
    // Add a 'name' getter that returns the first name or a default
    engine.register_get("name", |img: &mut Image| {
        if img.names.is_empty() {
            "<none>".to_string()
        } else {
            img.names[0].clone()
        }
    });
    engine.register_get("size", |img: &mut Image| img.size.clone());
    engine.register_get("created", |img: &mut Image| img.created.clone());
    
    Ok(())
}

/// Register legacy functions for backward compatibility
fn register_legacy_functions(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
    // Register container functions
    engine.register_fn("bah_from", bah_from_legacy);
    engine.register_fn("bah_run", bah_run_legacy);
    engine.register_fn("bah_run_with_isolation", bah_run_with_isolation_legacy);
    engine.register_fn("bah_copy", bah_copy_legacy);
    engine.register_fn("bah_add", bah_add_legacy);
    engine.register_fn("bah_commit", bah_commit_legacy);
    engine.register_fn("bah_remove", bah_remove_legacy);
    engine.register_fn("bah_list", bah_list_legacy);
    engine.register_fn("bah_build", bah_build_with_options_legacy);
    engine.register_fn("bah_new_build_options", new_build_options);
    
    // Register image functions
    engine.register_fn("bah_images", images_legacy);
    engine.register_fn("bah_image_remove", image_remove_legacy);
    engine.register_fn("bah_image_push", image_push_legacy);
    engine.register_fn("bah_image_tag", image_tag_legacy);
    engine.register_fn("bah_image_pull", image_pull_legacy);
    engine.register_fn("bah_image_commit", image_commit_with_options_legacy);
    engine.register_fn("bah_new_commit_options", new_commit_options);
    engine.register_fn("bah_config", config_with_options_legacy);
    engine.register_fn("bah_new_config_options", new_config_options);
    
    Ok(())
}

// Helper functions for error conversion
fn bah_error_to_rhai_error<T>(result: Result<T, BuildahError>) -> Result<T, Box<EvalAltResult>> {
    result.map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Buildah error: {}", e).into(),
            rhai::Position::NONE
        ))
    })
}

// Helper function to convert Rhai Map to Rust HashMap
fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<EvalAltResult>> {
    let mut config_options = HashMap::<String, String>::new();
    
    for (key, value) in options.iter() {
        if let Ok(value_str) = value.clone().into_string() {
            // Convert SmartString to String
            config_options.insert(key.to_string(), value_str);
        } else {
            return Err(Box::new(EvalAltResult::ErrorRuntime(
                format!("Option '{}' must be a string", key).into(),
                rhai::Position::NONE
            )));
        }
    }
    
    Ok(config_options)
}

/// Create a new Builder
pub fn bah_new(name: &str, image: &str) -> Result<Builder, Box<EvalAltResult>> {
    bah_error_to_rhai_error(Builder::new(name, image))
}

// Legacy function implementations (for backward compatibility)
// These would call the new Builder methods internally
// ...

4. Example Updates

Rust Example

// examples/buildah.rs
//! Example usage of the buildah module
//! 
//! This file demonstrates how to use the buildah module to perform
//! common container operations like creating containers, running commands,
//! and managing images.

use sal::virt::buildah::{Builder, BuildahError};
use std::collections::HashMap;

/// Run a complete buildah workflow example
pub fn run_buildah_example() -> Result<(), BuildahError> {
    println!("Starting buildah example workflow...");
    
    // Step 1: Create a container from an image using the Builder
    println!("\n=== Creating container from fedora:latest ===");
    let builder = Builder::new("my-fedora-container", "fedora:latest")?;
    println!("Created container: {}", builder.container_id().unwrap_or(&"unknown".to_string()));
    
    // Step 2: Run a command in the container
    println!("\n=== Installing nginx in container ===");
    // Use chroot isolation to avoid BPF issues
    let install_result = builder.run_with_isolation("dnf install -y nginx", "chroot")?;
    println!("{:#?}", install_result);
    println!("Installation output: {}", install_result.stdout);
    
    // Step 3: Copy a file into the container
    println!("\n=== Copying configuration file to container ===");
    builder.copy("./example.conf", "/etc/example.conf")?;
    
    // Step 4: Configure container metadata
    println!("\n=== Configuring container metadata ===");
    let mut config_options = HashMap::new();
    config_options.insert("port".to_string(), "80".to_string());
    config_options.insert("label".to_string(), "maintainer=example@example.com".to_string());
    config_options.insert("entrypoint".to_string(), "/usr/sbin/nginx".to_string());
    builder.config(config_options)?;
    println!("Container configured");
    
    // Step 5: Commit the container to create a new image
    println!("\n=== Committing container to create image ===");
    let image_name = "my-nginx:latest";
    builder.commit(image_name)?;
    println!("Created image: {}", image_name);
    
    // Step 6: List images to verify our new image exists
    println!("\n=== Listing images ===");
    let images = Builder::images()?;
    println!("Found {} images:", images.len());
    for image in images {
        println!("  ID: {}", image.id);
        println!("  Names: {}", image.names.join(", "));
        println!("  Size: {}", image.size);
        println!("  Created: {}", image.created);
        println!();
    }
    
    // Step 7: Clean up (optional in a real workflow)
    println!("\n=== Cleaning up ===");
    Builder::image_remove(image_name)?;
    builder.remove()?;
    
    println!("\nBuildah example workflow completed successfully!");
    Ok(())
}

/// Demonstrate how to build an image from a Containerfile/Dockerfile
pub fn build_image_example() -> Result<(), BuildahError> {
    println!("Building an image from a Containerfile...");
    
    // Use the build function with tag, context directory, and isolation to avoid BPF issues
    let result = Builder::build(Some("my-app:latest"), ".", "example_Dockerfile", Some("chroot"))?;
    
    println!("Build output: {}", result.stdout);
    println!("Image built successfully!");
    
    Ok(())
}

/// Example of pulling and pushing images
pub fn registry_operations_example() -> Result<(), BuildahError> {
    println!("Demonstrating registry operations...");
    
    // Pull an image
    println!("\n=== Pulling an image ===");
    Builder::image_pull("docker.io/library/alpine:latest", true)?;
    println!("Image pulled successfully");
    
    // Tag the image
    println!("\n=== Tagging the image ===");
    Builder::image_tag("alpine:latest", "my-alpine:v1.0")?;
    println!("Image tagged successfully");
    
    // Push an image (this would typically go to a real registry)
    // println!("\n=== Pushing an image (example only) ===");
    // println!("In a real scenario, you would push to a registry with:");
    // println!("Builder::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)");
    
    Ok(())
}

/// Main function to run all examples
pub fn run_all_examples() -> Result<(), BuildahError> {
    println!("=== BUILDAH MODULE EXAMPLES ===\n");
    
    run_buildah_example()?;
    build_image_example()?;
    registry_operations_example()?;

    Ok(())
}

fn main() {
    let _ = run_all_examples();
}

Rhai Example

// rhaiexamples/04_buildah_operations.rhai
// Demonstrates container operations using SAL's buildah integration
// Note: This script requires buildah to be installed and may need root privileges

// Check if buildah is installed
let buildah_exists = which("buildah");
println(`Buildah exists: ${buildah_exists}`);